@vibedrift/cli 0.4.5 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1342 -946
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -598,63 +598,72 @@ function extractAssignedVariable(line) {
|
|
|
598
598
|
if (pyMatch) return pyMatch[1];
|
|
599
599
|
return null;
|
|
600
600
|
}
|
|
601
|
-
function
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
601
|
+
function identifySources(trimmed, langSources, lineNumber, taintedVars) {
|
|
602
|
+
for (const src of langSources) {
|
|
603
|
+
if (src.regex.test(trimmed)) {
|
|
604
|
+
const varName = extractAssignedVariable(trimmed);
|
|
605
|
+
if (varName) {
|
|
606
|
+
taintedVars.set(varName, {
|
|
607
|
+
name: varName,
|
|
608
|
+
source: { type: src.label, variable: varName, line: lineNumber },
|
|
609
|
+
sanitizedFor: /* @__PURE__ */ new Set()
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function checkSanitizers(trimmed, taintedVars) {
|
|
616
|
+
for (const [varName, tainted] of taintedVars) {
|
|
617
|
+
if (!trimmed.includes(varName)) continue;
|
|
618
|
+
for (const san of SANITIZERS) {
|
|
619
|
+
if (san.regex.test(trimmed)) {
|
|
620
|
+
if (san.removes === "all") {
|
|
621
|
+
tainted.sanitizedFor = new Set(ALL_TAINT_CATEGORIES);
|
|
622
|
+
} else {
|
|
623
|
+
tainted.sanitizedFor.add(san.removes);
|
|
618
624
|
}
|
|
619
625
|
}
|
|
620
626
|
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function identifySinks(trimmed, fn, lineNumber, taintedVars, flows) {
|
|
630
|
+
for (const sink of TAINT_SINKS) {
|
|
631
|
+
if (!sink.regex.test(trimmed)) continue;
|
|
621
632
|
for (const [varName, tainted] of taintedVars) {
|
|
622
633
|
if (!trimmed.includes(varName)) continue;
|
|
634
|
+
if (tainted.sanitizedFor.has(sink.category)) continue;
|
|
635
|
+
let inlineSanitized = false;
|
|
623
636
|
for (const san of SANITIZERS) {
|
|
624
|
-
if (san.regex.test(trimmed)) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
} else {
|
|
628
|
-
tainted.sanitizedFor.add(san.removes);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
for (const sink of TAINT_SINKS) {
|
|
634
|
-
if (!sink.regex.test(trimmed)) continue;
|
|
635
|
-
for (const [varName, tainted] of taintedVars) {
|
|
636
|
-
if (!trimmed.includes(varName)) continue;
|
|
637
|
-
if (tainted.sanitizedFor.has(sink.category)) continue;
|
|
638
|
-
let inlineSanitized = false;
|
|
639
|
-
for (const san of SANITIZERS) {
|
|
640
|
-
if (san.regex.test(trimmed) && (san.removes === "all" || san.removes === sink.category)) {
|
|
641
|
-
inlineSanitized = true;
|
|
642
|
-
break;
|
|
643
|
-
}
|
|
637
|
+
if (san.regex.test(trimmed) && (san.removes === "all" || san.removes === sink.category)) {
|
|
638
|
+
inlineSanitized = true;
|
|
639
|
+
break;
|
|
644
640
|
}
|
|
645
|
-
if (inlineSanitized) continue;
|
|
646
|
-
flows.push({
|
|
647
|
-
file: fn.file,
|
|
648
|
-
relativePath: fn.relativePath,
|
|
649
|
-
functionName: fn.name,
|
|
650
|
-
source: tainted.source,
|
|
651
|
-
sink: { type: sink.label, expression: trimmed.slice(0, 100), line: fn.line + i, severity: sink.severity },
|
|
652
|
-
sanitized: false,
|
|
653
|
-
language: fn.language
|
|
654
|
-
});
|
|
655
641
|
}
|
|
642
|
+
if (inlineSanitized) continue;
|
|
643
|
+
flows.push({
|
|
644
|
+
file: fn.file,
|
|
645
|
+
relativePath: fn.relativePath,
|
|
646
|
+
functionName: fn.name,
|
|
647
|
+
source: tainted.source,
|
|
648
|
+
sink: { type: sink.label, expression: trimmed.slice(0, 100), line: lineNumber, severity: sink.severity },
|
|
649
|
+
sanitized: false,
|
|
650
|
+
language: fn.language
|
|
651
|
+
});
|
|
656
652
|
}
|
|
657
653
|
}
|
|
654
|
+
}
|
|
655
|
+
function analyzeFunction(fn) {
|
|
656
|
+
const flows = [];
|
|
657
|
+
const lines = fn.rawBody.split("\n");
|
|
658
|
+
const taintedVars = /* @__PURE__ */ new Map();
|
|
659
|
+
const langSources = TAINT_SOURCES[fn.language] ?? [];
|
|
660
|
+
for (let i = 0; i < lines.length; i++) {
|
|
661
|
+
const trimmed = lines[i].trim();
|
|
662
|
+
const lineNumber = fn.line + i;
|
|
663
|
+
identifySources(trimmed, langSources, lineNumber, taintedVars);
|
|
664
|
+
checkSanitizers(trimmed, taintedVars);
|
|
665
|
+
identifySinks(trimmed, fn, lineNumber, taintedVars, flows);
|
|
666
|
+
}
|
|
658
667
|
return flows;
|
|
659
668
|
}
|
|
660
669
|
function analyzeTaintFlows(functions) {
|
|
@@ -683,7 +692,7 @@ function taintFindings(flows) {
|
|
|
683
692
|
tags: ["codedna", "taint", "security"]
|
|
684
693
|
}));
|
|
685
694
|
}
|
|
686
|
-
var TAINT_SOURCES, TAINT_SINKS, SANITIZERS;
|
|
695
|
+
var TAINT_SOURCES, TAINT_SINKS, SANITIZERS, ALL_TAINT_CATEGORIES;
|
|
687
696
|
var init_taint_analysis = __esm({
|
|
688
697
|
"src/codedna/taint-analysis.ts"() {
|
|
689
698
|
"use strict";
|
|
@@ -779,6 +788,7 @@ var init_taint_analysis = __esm({
|
|
|
779
788
|
{ regex: /path\.(?:join|resolve|normalize)\s*\(/, label: "path.join/resolve", removes: "path_traversal" },
|
|
780
789
|
{ regex: /filepath\.(?:Clean|Abs)\s*\(/, label: "filepath.Clean", removes: "path_traversal" }
|
|
781
790
|
];
|
|
791
|
+
ALL_TAINT_CATEGORIES = /* @__PURE__ */ new Set(["sql_injection", "command_injection", "path_traversal", "xss", "ssrf", "code_injection"]);
|
|
782
792
|
}
|
|
783
793
|
});
|
|
784
794
|
|
|
@@ -803,6 +813,57 @@ function hasExplanatoryComment(content, lines, deviationLine) {
|
|
|
803
813
|
function isInSpecialDirectory(path2) {
|
|
804
814
|
return SPECIAL_DIRS.test(path2);
|
|
805
815
|
}
|
|
816
|
+
function computeSignalScore(file, devDist, dominantFiles, projectDominant) {
|
|
817
|
+
const lines = file.content.split("\n");
|
|
818
|
+
const signals = [];
|
|
819
|
+
let totalWeight = 0;
|
|
820
|
+
const sqlComplexity = countComplexSqlSignals(file.content);
|
|
821
|
+
if (sqlComplexity > 0) {
|
|
822
|
+
const weight = Math.min(sqlComplexity * 0.15, 0.3);
|
|
823
|
+
signals.push({ type: "complex_sql", present: true, weight, evidence: `${sqlComplexity} complex SQL indicators` });
|
|
824
|
+
totalWeight += weight;
|
|
825
|
+
} else {
|
|
826
|
+
signals.push({ type: "complex_sql", present: false, weight: 0 });
|
|
827
|
+
}
|
|
828
|
+
const firstSignalLine = devDist.signals[0]?.line;
|
|
829
|
+
if (hasExplanatoryComment(file.content, lines, firstSignalLine)) {
|
|
830
|
+
signals.push({ type: "explanatory_comment", present: true, weight: 0.2, evidence: "comment explains deviation" });
|
|
831
|
+
totalWeight += 0.2;
|
|
832
|
+
} else {
|
|
833
|
+
signals.push({ type: "no_comment", present: true, weight: -0.1, evidence: "no explanatory comment" });
|
|
834
|
+
totalWeight -= 0.1;
|
|
835
|
+
}
|
|
836
|
+
if (isInSpecialDirectory(devDist.relativePath)) {
|
|
837
|
+
signals.push({ type: "special_directory", present: true, weight: 0.2, evidence: devDist.relativePath });
|
|
838
|
+
totalWeight += 0.2;
|
|
839
|
+
}
|
|
840
|
+
if (devDist.dominantPattern === "raw_sql" && sqlComplexity === 0) {
|
|
841
|
+
const hasCrudOnly = /(?:SELECT\s+\*|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)/i.test(file.content);
|
|
842
|
+
if (hasCrudOnly) {
|
|
843
|
+
signals.push({ type: "simple_crud", present: true, weight: -0.3, evidence: "simple CRUD SQL without complex operations" });
|
|
844
|
+
totalWeight -= 0.3;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const devDir = devDist.relativePath.includes("/") ? devDist.relativePath.slice(0, devDist.relativePath.lastIndexOf("/")) : ".";
|
|
848
|
+
const sameDir = dominantFiles.filter((d) => {
|
|
849
|
+
const dir = d.relativePath.includes("/") ? d.relativePath.slice(0, d.relativePath.lastIndexOf("/")) : ".";
|
|
850
|
+
return dir === devDir;
|
|
851
|
+
});
|
|
852
|
+
if (sameDir.length > 0) {
|
|
853
|
+
signals.push({ type: "same_directory_as_dominant", present: true, weight: -0.2, evidence: `${sameDir.length} files in same directory use ${projectDominant}` });
|
|
854
|
+
totalWeight -= 0.2;
|
|
855
|
+
}
|
|
856
|
+
return { signals, totalWeight };
|
|
857
|
+
}
|
|
858
|
+
function classifyDeviation(totalWeight) {
|
|
859
|
+
const rawScore = 0.5 + totalWeight;
|
|
860
|
+
const justificationScore = Math.max(0, Math.min(1, rawScore));
|
|
861
|
+
let verdict;
|
|
862
|
+
if (justificationScore >= 0.6) verdict = "likely_justified";
|
|
863
|
+
else if (justificationScore <= 0.3) verdict = "likely_accidental";
|
|
864
|
+
else verdict = "uncertain";
|
|
865
|
+
return { justificationScore, verdict };
|
|
866
|
+
}
|
|
806
867
|
function scoreDeviations(distributions, files) {
|
|
807
868
|
if (distributions.length < 2) return [];
|
|
808
869
|
const patternCounts = /* @__PURE__ */ new Map();
|
|
@@ -824,51 +885,8 @@ function scoreDeviations(distributions, files) {
|
|
|
824
885
|
for (const devDist of deviatingFiles) {
|
|
825
886
|
const file = files.find((f) => f.path === devDist.file || f.relativePath === devDist.relativePath);
|
|
826
887
|
if (!file) continue;
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
let totalWeight = 0;
|
|
830
|
-
const sqlComplexity = countComplexSqlSignals(file.content);
|
|
831
|
-
if (sqlComplexity > 0) {
|
|
832
|
-
const weight = Math.min(sqlComplexity * 0.15, 0.3);
|
|
833
|
-
signals.push({ type: "complex_sql", present: true, weight, evidence: `${sqlComplexity} complex SQL indicators` });
|
|
834
|
-
totalWeight += weight;
|
|
835
|
-
} else {
|
|
836
|
-
signals.push({ type: "complex_sql", present: false, weight: 0 });
|
|
837
|
-
}
|
|
838
|
-
const firstSignalLine = devDist.signals[0]?.line;
|
|
839
|
-
if (hasExplanatoryComment(file.content, lines, firstSignalLine)) {
|
|
840
|
-
signals.push({ type: "explanatory_comment", present: true, weight: 0.2, evidence: "comment explains deviation" });
|
|
841
|
-
totalWeight += 0.2;
|
|
842
|
-
} else {
|
|
843
|
-
signals.push({ type: "no_comment", present: true, weight: -0.1, evidence: "no explanatory comment" });
|
|
844
|
-
totalWeight -= 0.1;
|
|
845
|
-
}
|
|
846
|
-
if (isInSpecialDirectory(devDist.relativePath)) {
|
|
847
|
-
signals.push({ type: "special_directory", present: true, weight: 0.2, evidence: devDist.relativePath });
|
|
848
|
-
totalWeight += 0.2;
|
|
849
|
-
}
|
|
850
|
-
if (devDist.dominantPattern === "raw_sql" && sqlComplexity === 0) {
|
|
851
|
-
const hasCrudOnly = /(?:SELECT\s+\*|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)/i.test(file.content);
|
|
852
|
-
if (hasCrudOnly) {
|
|
853
|
-
signals.push({ type: "simple_crud", present: true, weight: -0.3, evidence: "simple CRUD SQL without complex operations" });
|
|
854
|
-
totalWeight -= 0.3;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
const devDir = devDist.relativePath.includes("/") ? devDist.relativePath.slice(0, devDist.relativePath.lastIndexOf("/")) : ".";
|
|
858
|
-
const sameDir = dominantFiles.filter((d) => {
|
|
859
|
-
const dir = d.relativePath.includes("/") ? d.relativePath.slice(0, d.relativePath.lastIndexOf("/")) : ".";
|
|
860
|
-
return dir === devDir;
|
|
861
|
-
});
|
|
862
|
-
if (sameDir.length > 0) {
|
|
863
|
-
signals.push({ type: "same_directory_as_dominant", present: true, weight: -0.2, evidence: `${sameDir.length} files in same directory use ${projectDominant}` });
|
|
864
|
-
totalWeight -= 0.2;
|
|
865
|
-
}
|
|
866
|
-
const rawScore = 0.5 + totalWeight;
|
|
867
|
-
const justificationScore = Math.max(0, Math.min(1, rawScore));
|
|
868
|
-
let verdict;
|
|
869
|
-
if (justificationScore >= 0.6) verdict = "likely_justified";
|
|
870
|
-
else if (justificationScore <= 0.3) verdict = "likely_accidental";
|
|
871
|
-
else verdict = "uncertain";
|
|
888
|
+
const { signals, totalWeight } = computeSignalScore(file, devDist, dominantFiles, projectDominant);
|
|
889
|
+
const { justificationScore, verdict } = classifyDeviation(totalWeight);
|
|
872
890
|
justifications.push({
|
|
873
891
|
file: devDist.file,
|
|
874
892
|
relativePath: devDist.relativePath,
|
|
@@ -1664,10 +1682,8 @@ var sanitize_result_exports = {};
|
|
|
1664
1682
|
__export(sanitize_result_exports, {
|
|
1665
1683
|
sanitizeResultForUpload: () => sanitizeResultForUpload
|
|
1666
1684
|
});
|
|
1667
|
-
function
|
|
1668
|
-
|
|
1669
|
-
const rootDir = ctx?.rootDir ?? "";
|
|
1670
|
-
const stripPath = (p) => {
|
|
1685
|
+
function createStripPath(rootDir) {
|
|
1686
|
+
return (p) => {
|
|
1671
1687
|
if (!p) return null;
|
|
1672
1688
|
if (rootDir && p.startsWith(rootDir)) {
|
|
1673
1689
|
const rel = p.slice(rootDir.length).replace(/^\/+/, "");
|
|
@@ -1675,44 +1691,60 @@ function sanitizeResultForUpload(result) {
|
|
|
1675
1691
|
}
|
|
1676
1692
|
return p;
|
|
1677
1693
|
};
|
|
1694
|
+
}
|
|
1695
|
+
function createSanitizeNode(stripPath) {
|
|
1678
1696
|
const sanitizeNode = (node) => {
|
|
1679
1697
|
if (typeof node === "string") return stripPath(node) ?? node;
|
|
1680
1698
|
if (Array.isArray(node)) return node.map(sanitizeNode);
|
|
1681
|
-
if (node && typeof node === "object") {
|
|
1682
|
-
const out = {};
|
|
1683
|
-
for (const [k, v] of Object.entries(node)) {
|
|
1684
|
-
if (k === "rootDir") continue;
|
|
1685
|
-
if (k === "files" && Array.isArray(v)) {
|
|
1686
|
-
out[k] = v.map((f) => ({
|
|
1687
|
-
relativePath: stripPath(f.relativePath) ?? f.relativePath,
|
|
1688
|
-
lineCount: f.lineCount,
|
|
1689
|
-
language: f.language
|
|
1690
|
-
}));
|
|
1691
|
-
continue;
|
|
1692
|
-
}
|
|
1693
|
-
if (k === "ast" || k === "treeSitterNode") continue;
|
|
1694
|
-
out[k] = sanitizeNode(v);
|
|
1695
|
-
}
|
|
1696
|
-
return out;
|
|
1697
|
-
}
|
|
1698
1699
|
if (node instanceof Map) {
|
|
1699
|
-
|
|
1700
|
-
for (const [k, v] of node.entries()) {
|
|
1701
|
-
const safeKey = typeof k === "string" ? stripPath(k) ?? k : String(k);
|
|
1702
|
-
obj[safeKey] = sanitizeNode(v);
|
|
1703
|
-
}
|
|
1704
|
-
return obj;
|
|
1700
|
+
return sanitizeMapNode(node, stripPath, sanitizeNode);
|
|
1705
1701
|
}
|
|
1706
1702
|
if (node instanceof Set) {
|
|
1707
1703
|
return [...node].map(sanitizeNode);
|
|
1708
1704
|
}
|
|
1705
|
+
if (node && typeof node === "object") {
|
|
1706
|
+
return sanitizeObjectNode(node, stripPath, sanitizeNode);
|
|
1707
|
+
}
|
|
1709
1708
|
return node;
|
|
1710
1709
|
};
|
|
1711
|
-
|
|
1710
|
+
return sanitizeNode;
|
|
1711
|
+
}
|
|
1712
|
+
function sanitizeObjectNode(node, stripPath, sanitizeNode) {
|
|
1713
|
+
const out = {};
|
|
1714
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1715
|
+
if (k === "rootDir") continue;
|
|
1716
|
+
if (k === "files" && Array.isArray(v)) {
|
|
1717
|
+
out[k] = sanitizeFilesList(v, stripPath);
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
if (k === "ast" || k === "treeSitterNode") continue;
|
|
1721
|
+
out[k] = sanitizeNode(v);
|
|
1722
|
+
}
|
|
1723
|
+
return out;
|
|
1724
|
+
}
|
|
1725
|
+
function sanitizeFilesList(files, stripPath) {
|
|
1726
|
+
return files.map((f) => ({
|
|
1727
|
+
relativePath: stripPath(f.relativePath) ?? f.relativePath,
|
|
1728
|
+
lineCount: f.lineCount,
|
|
1729
|
+
language: f.language
|
|
1730
|
+
}));
|
|
1731
|
+
}
|
|
1732
|
+
function sanitizeMapNode(node, stripPath, sanitizeNode) {
|
|
1733
|
+
const obj = {};
|
|
1734
|
+
for (const [k, v] of node.entries()) {
|
|
1735
|
+
const safeKey = typeof k === "string" ? stripPath(k) ?? k : String(k);
|
|
1736
|
+
obj[safeKey] = sanitizeNode(v);
|
|
1737
|
+
}
|
|
1738
|
+
return obj;
|
|
1739
|
+
}
|
|
1740
|
+
function sanitizeResultForUpload(result) {
|
|
1741
|
+
const ctx = result.context;
|
|
1742
|
+
const rootDir = ctx?.rootDir ?? "";
|
|
1743
|
+
const stripPath = createStripPath(rootDir);
|
|
1744
|
+
const sanitizeNode = createSanitizeNode(stripPath);
|
|
1745
|
+
return {
|
|
1712
1746
|
schema: "vibedrift-scan-result/v1",
|
|
1713
|
-
project: {
|
|
1714
|
-
// populated by the caller, since the CLI knows project_name + hash
|
|
1715
|
-
},
|
|
1747
|
+
project: {},
|
|
1716
1748
|
language: {
|
|
1717
1749
|
dominant: ctx?.dominantLanguage ?? null,
|
|
1718
1750
|
breakdown: sanitizeNode(ctx?.languageBreakdown),
|
|
@@ -1734,7 +1766,6 @@ function sanitizeResultForUpload(result) {
|
|
|
1734
1766
|
aiSummary: result.aiSummary ?? null,
|
|
1735
1767
|
scanTimeMs: result.scanTimeMs
|
|
1736
1768
|
};
|
|
1737
|
-
return envelope;
|
|
1738
1769
|
}
|
|
1739
1770
|
var init_sanitize_result = __esm({
|
|
1740
1771
|
"src/ml-client/sanitize-result.ts"() {
|
|
@@ -1757,20 +1788,23 @@ function csvEscape(val) {
|
|
|
1757
1788
|
function row(...cells) {
|
|
1758
1789
|
return cells.map((c) => csvEscape(String(c))).join(",");
|
|
1759
1790
|
}
|
|
1760
|
-
function
|
|
1791
|
+
function csvMetadata(result) {
|
|
1792
|
+
return [
|
|
1793
|
+
"VIBEDRIFT REPORT",
|
|
1794
|
+
row("Project", result.context.rootDir.split("/").pop() ?? ""),
|
|
1795
|
+
row("Files Scanned", result.context.files.length),
|
|
1796
|
+
row("Total Lines", result.context.totalLines),
|
|
1797
|
+
row("Scan Time (ms)", result.scanTimeMs),
|
|
1798
|
+
row("Composite Score", result.compositeScore),
|
|
1799
|
+
row("Max Score", result.maxCompositeScore),
|
|
1800
|
+
""
|
|
1801
|
+
];
|
|
1802
|
+
}
|
|
1803
|
+
function csvScoreCategories(result) {
|
|
1761
1804
|
const lines = [];
|
|
1762
|
-
lines.push("VIBEDRIFT REPORT");
|
|
1763
|
-
lines.push(row("Project", result.context.rootDir.split("/").pop() ?? ""));
|
|
1764
|
-
lines.push(row("Files Scanned", result.context.files.length));
|
|
1765
|
-
lines.push(row("Total Lines", result.context.totalLines));
|
|
1766
|
-
lines.push(row("Scan Time (ms)", result.scanTimeMs));
|
|
1767
|
-
lines.push(row("Composite Score", result.compositeScore));
|
|
1768
|
-
lines.push(row("Max Score", result.maxCompositeScore));
|
|
1769
|
-
lines.push("");
|
|
1770
1805
|
lines.push("CATEGORY SCORES");
|
|
1771
1806
|
lines.push(row("Category", "Score", "Max Score", "Finding Count"));
|
|
1772
|
-
const
|
|
1773
|
-
for (const [key, val] of Object.entries(cs)) {
|
|
1807
|
+
for (const [key, val] of Object.entries(result.scores)) {
|
|
1774
1808
|
lines.push(row(key, val.score, val.maxScore, val.findingCount));
|
|
1775
1809
|
}
|
|
1776
1810
|
lines.push("");
|
|
@@ -1787,95 +1821,125 @@ function renderCsvReport(result) {
|
|
|
1787
1821
|
}
|
|
1788
1822
|
lines.push("");
|
|
1789
1823
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1824
|
+
return lines;
|
|
1825
|
+
}
|
|
1826
|
+
function csvDriftFindings(result) {
|
|
1827
|
+
if ((result.driftFindings ?? []).length === 0) return [];
|
|
1828
|
+
const lines = [];
|
|
1829
|
+
lines.push("DRIFT FINDINGS");
|
|
1830
|
+
lines.push(row("Severity", "Category", "Finding", "Dominant Pattern", "Dominant Count", "Total Files", "Consistency %", "Deviating Files", "Recommendation"));
|
|
1831
|
+
for (const d of result.driftFindings) {
|
|
1832
|
+
const devFiles = d.deviatingFiles.map((f) => f.path).join("; ");
|
|
1833
|
+
lines.push(row(
|
|
1834
|
+
d.severity,
|
|
1835
|
+
d.driftCategory,
|
|
1836
|
+
d.finding,
|
|
1837
|
+
d.dominantPattern,
|
|
1838
|
+
d.dominantCount,
|
|
1839
|
+
d.totalRelevantFiles,
|
|
1840
|
+
d.consistencyScore,
|
|
1841
|
+
devFiles,
|
|
1842
|
+
d.recommendation
|
|
1843
|
+
));
|
|
1808
1844
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
));
|
|
1875
|
-
}
|
|
1876
|
-
lines.push("");
|
|
1877
|
-
}
|
|
1845
|
+
lines.push("");
|
|
1846
|
+
return lines;
|
|
1847
|
+
}
|
|
1848
|
+
function csvDuplicateGroups(dna) {
|
|
1849
|
+
if (!dna.duplicateGroups?.length) return [];
|
|
1850
|
+
const lines = [];
|
|
1851
|
+
lines.push("CODE DNA: SEMANTIC DUPLICATES");
|
|
1852
|
+
lines.push(row("Group", "Functions", "Files"));
|
|
1853
|
+
for (const g of dna.duplicateGroups) {
|
|
1854
|
+
const fns = g.functions.map((f) => f.name + "()").join("; ");
|
|
1855
|
+
const files = g.functions.map((f) => f.relativePath || f.file).join("; ");
|
|
1856
|
+
lines.push(row(g.groupId, fns, files));
|
|
1857
|
+
}
|
|
1858
|
+
lines.push("");
|
|
1859
|
+
return lines;
|
|
1860
|
+
}
|
|
1861
|
+
function csvSequenceSimilarities(dna) {
|
|
1862
|
+
if (!dna.sequenceSimilarities?.length) return [];
|
|
1863
|
+
const lines = [];
|
|
1864
|
+
lines.push("CODE DNA: OPERATION SEQUENCE MATCHES");
|
|
1865
|
+
lines.push(row("Function A", "File A", "Function B", "File B", "Similarity %"));
|
|
1866
|
+
for (const s of dna.sequenceSimilarities) {
|
|
1867
|
+
lines.push(row(
|
|
1868
|
+
s.functionA.name,
|
|
1869
|
+
s.functionA.relativePath || s.functionA.file,
|
|
1870
|
+
s.functionB.name,
|
|
1871
|
+
s.functionB.relativePath || s.functionB.file,
|
|
1872
|
+
Math.round(s.similarity * 100)
|
|
1873
|
+
));
|
|
1874
|
+
}
|
|
1875
|
+
lines.push("");
|
|
1876
|
+
return lines;
|
|
1877
|
+
}
|
|
1878
|
+
function csvTaintFlows(dna) {
|
|
1879
|
+
if (!dna.taintFlows?.length) return [];
|
|
1880
|
+
const lines = [];
|
|
1881
|
+
lines.push("CODE DNA: TAINT FLOWS");
|
|
1882
|
+
lines.push(row("File", "Function", "Source Type", "Source Line", "Sink Type", "Sink Line", "Sanitized"));
|
|
1883
|
+
for (const t of dna.taintFlows) {
|
|
1884
|
+
lines.push(row(
|
|
1885
|
+
t.relativePath || t.file,
|
|
1886
|
+
t.functionName,
|
|
1887
|
+
t.source.type,
|
|
1888
|
+
t.source.line,
|
|
1889
|
+
t.sink.type,
|
|
1890
|
+
t.sink.line,
|
|
1891
|
+
t.sanitized ? "Yes" : "No"
|
|
1892
|
+
));
|
|
1893
|
+
}
|
|
1894
|
+
lines.push("");
|
|
1895
|
+
return lines;
|
|
1896
|
+
}
|
|
1897
|
+
function csvDeviations(dna) {
|
|
1898
|
+
if (!dna.deviationJustifications?.length) return [];
|
|
1899
|
+
const lines = [];
|
|
1900
|
+
lines.push("CODE DNA: DEVIATION ANALYSIS");
|
|
1901
|
+
lines.push(row("File", "Deviating Pattern", "Dominant Pattern", "Verdict", "Score"));
|
|
1902
|
+
for (const dj of dna.deviationJustifications) {
|
|
1903
|
+
lines.push(row(
|
|
1904
|
+
dj.relativePath || dj.file,
|
|
1905
|
+
dj.deviatingPattern,
|
|
1906
|
+
dj.dominantPattern,
|
|
1907
|
+
dj.verdict,
|
|
1908
|
+
Math.round(dj.justificationScore * 100)
|
|
1909
|
+
));
|
|
1878
1910
|
}
|
|
1911
|
+
lines.push("");
|
|
1912
|
+
return lines;
|
|
1913
|
+
}
|
|
1914
|
+
function csvPatterns(dna) {
|
|
1915
|
+
if (!dna.patternDistributions?.length) return [];
|
|
1916
|
+
const lines = [];
|
|
1917
|
+
lines.push("CODE DNA: PATTERN DISTRIBUTIONS");
|
|
1918
|
+
lines.push(row("File", "Dominant Pattern", "Confidence", "Internally Inconsistent"));
|
|
1919
|
+
for (const pd of dna.patternDistributions) {
|
|
1920
|
+
lines.push(row(
|
|
1921
|
+
pd.relativePath || pd.file,
|
|
1922
|
+
pd.dominantPattern,
|
|
1923
|
+
Math.round(pd.confidence * 100),
|
|
1924
|
+
pd.isInternallyInconsistent ? "Yes" : "No"
|
|
1925
|
+
));
|
|
1926
|
+
}
|
|
1927
|
+
lines.push("");
|
|
1928
|
+
return lines;
|
|
1929
|
+
}
|
|
1930
|
+
function csvCodeDna(result) {
|
|
1931
|
+
const dna = result.codeDnaResult;
|
|
1932
|
+
if (!dna) return [];
|
|
1933
|
+
return [
|
|
1934
|
+
...csvDuplicateGroups(dna),
|
|
1935
|
+
...csvSequenceSimilarities(dna),
|
|
1936
|
+
...csvTaintFlows(dna),
|
|
1937
|
+
...csvDeviations(dna),
|
|
1938
|
+
...csvPatterns(dna)
|
|
1939
|
+
];
|
|
1940
|
+
}
|
|
1941
|
+
function csvFindings(result) {
|
|
1942
|
+
const lines = [];
|
|
1879
1943
|
lines.push("ALL FINDINGS");
|
|
1880
1944
|
lines.push(row("Severity", "Analyzer", "Confidence %", "Message", "File", "Line", "Tags"));
|
|
1881
1945
|
for (const f of result.findings) {
|
|
@@ -1891,6 +1955,10 @@ function renderCsvReport(result) {
|
|
|
1891
1955
|
));
|
|
1892
1956
|
}
|
|
1893
1957
|
lines.push("");
|
|
1958
|
+
return lines;
|
|
1959
|
+
}
|
|
1960
|
+
function csvPerFileScores(result) {
|
|
1961
|
+
const lines = [];
|
|
1894
1962
|
lines.push("PER-FILE SCORES");
|
|
1895
1963
|
lines.push(row("File", "Score", "Finding Count"));
|
|
1896
1964
|
const fileSorted = [...result.perFileScores.entries()].sort((a, b) => a[1].score - b[1].score);
|
|
@@ -1898,21 +1966,35 @@ function renderCsvReport(result) {
|
|
|
1898
1966
|
lines.push(row(path2, data.score, data.findings.length));
|
|
1899
1967
|
}
|
|
1900
1968
|
lines.push("");
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1969
|
+
return lines;
|
|
1970
|
+
}
|
|
1971
|
+
function csvAiSummary(result) {
|
|
1972
|
+
if ((result.deepInsights ?? []).length === 0) return [];
|
|
1973
|
+
const lines = [];
|
|
1974
|
+
lines.push("DEEP ANALYSIS INSIGHTS");
|
|
1975
|
+
lines.push(row("Category", "Severity", "Title", "Description", "Related Files", "Recommendation"));
|
|
1976
|
+
for (const ins of result.deepInsights) {
|
|
1977
|
+
lines.push(row(
|
|
1978
|
+
ins.category,
|
|
1979
|
+
ins.severity,
|
|
1980
|
+
ins.title,
|
|
1981
|
+
ins.description,
|
|
1982
|
+
ins.relatedFiles.join("; "),
|
|
1983
|
+
ins.recommendation ?? ""
|
|
1984
|
+
));
|
|
1914
1985
|
}
|
|
1915
|
-
return lines
|
|
1986
|
+
return lines;
|
|
1987
|
+
}
|
|
1988
|
+
function renderCsvReport(result) {
|
|
1989
|
+
return [
|
|
1990
|
+
...csvMetadata(result),
|
|
1991
|
+
...csvScoreCategories(result),
|
|
1992
|
+
...csvDriftFindings(result),
|
|
1993
|
+
...csvCodeDna(result),
|
|
1994
|
+
...csvFindings(result),
|
|
1995
|
+
...csvPerFileScores(result),
|
|
1996
|
+
...csvAiSummary(result)
|
|
1997
|
+
].join("\n");
|
|
1916
1998
|
}
|
|
1917
1999
|
var init_csv = __esm({
|
|
1918
2000
|
"src/output/csv.ts"() {
|
|
@@ -2048,7 +2130,10 @@ function tableRow(cells) {
|
|
|
2048
2130
|
function hr() {
|
|
2049
2131
|
return `<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="4" w:space="1" w:color="CCCCCC"/></w:pBdr></w:pPr></w:p>`;
|
|
2050
2132
|
}
|
|
2051
|
-
function
|
|
2133
|
+
function wrapTable(rows) {
|
|
2134
|
+
return `<w:tbl><w:tblPr>${TABLE_BORDERS}<w:tblW w:w="5000" w:type="pct"/></w:tblPr>${rows}</w:tbl>`;
|
|
2135
|
+
}
|
|
2136
|
+
function buildDocxTitlePage(result) {
|
|
2052
2137
|
const parts = [];
|
|
2053
2138
|
const name = result.context.rootDir.split("/").pop() ?? "project";
|
|
2054
2139
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
|
|
@@ -2058,6 +2143,10 @@ function buildDocumentXml(result) {
|
|
|
2058
2143
|
const langs = [...result.context.languageBreakdown.entries()].map(([l, s]) => `${l}: ${s.files} files`).join(", ");
|
|
2059
2144
|
parts.push(para(`Languages: ${langs}`));
|
|
2060
2145
|
parts.push(hr());
|
|
2146
|
+
return parts.join("\n ");
|
|
2147
|
+
}
|
|
2148
|
+
function buildDocxScoreTable(result) {
|
|
2149
|
+
const parts = [];
|
|
2061
2150
|
const pct = result.maxCompositeScore > 0 ? result.compositeScore / result.maxCompositeScore * 100 : 0;
|
|
2062
2151
|
const grade = pct >= 90 ? "A" : pct >= 75 ? "B" : pct >= 50 ? "C" : pct >= 25 ? "D" : "F";
|
|
2063
2152
|
parts.push(para("1. SCORE SUMMARY", "Heading1"));
|
|
@@ -2071,24 +2160,27 @@ function buildDocumentXml(result) {
|
|
|
2071
2160
|
["Convention Drift", ds.naming_conventions],
|
|
2072
2161
|
["Phantom Scaffolding", ds.phantom_scaffolding]
|
|
2073
2162
|
];
|
|
2074
|
-
|
|
2163
|
+
const headerRow = tableRow([
|
|
2075
2164
|
tableCellSimple("Category", "D9E2F3", true),
|
|
2076
2165
|
tableCellSimple("Score", "D9E2F3", true),
|
|
2077
2166
|
tableCellSimple("Max", "D9E2F3", true),
|
|
2078
2167
|
tableCellSimple("Findings", "D9E2F3", true)
|
|
2079
|
-
])
|
|
2080
|
-
|
|
2168
|
+
]);
|
|
2169
|
+
const dataRows = catRows.map(([catName, catScore]) => {
|
|
2081
2170
|
const cs = catScore;
|
|
2082
|
-
|
|
2171
|
+
return tableRow([
|
|
2083
2172
|
tableCellSimple(catName),
|
|
2084
2173
|
tableCellSimple(String(cs?.score ?? 0)),
|
|
2085
2174
|
tableCellSimple(String(cs?.maxScore ?? 0)),
|
|
2086
2175
|
tableCellSimple(String(cs?.findings ?? 0))
|
|
2087
|
-
])
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
parts.push(scoreTable);
|
|
2176
|
+
]);
|
|
2177
|
+
}).join("");
|
|
2178
|
+
parts.push(wrapTable(headerRow + dataRows));
|
|
2091
2179
|
parts.push(hr());
|
|
2180
|
+
return parts.join("\n ");
|
|
2181
|
+
}
|
|
2182
|
+
function buildDocxIntentSection(result) {
|
|
2183
|
+
const parts = [];
|
|
2092
2184
|
parts.push(para("2. CODEBASE INTENT", "Heading1"));
|
|
2093
2185
|
parts.push(para("The following patterns represent the dominant approach established by the majority of files in this codebase."));
|
|
2094
2186
|
parts.push(para(""));
|
|
@@ -2102,6 +2194,10 @@ function buildDocumentXml(result) {
|
|
|
2102
2194
|
}
|
|
2103
2195
|
if (intentSeen.size === 0) parts.push(para("No dominant patterns detected \u2014 codebase may be too small or highly consistent."));
|
|
2104
2196
|
parts.push(hr());
|
|
2197
|
+
return parts.join("\n ");
|
|
2198
|
+
}
|
|
2199
|
+
function buildDocxDriftSection(result) {
|
|
2200
|
+
const parts = [];
|
|
2105
2201
|
parts.push(para("3. DRIFT FINDINGS", "Heading1"));
|
|
2106
2202
|
parts.push(para(`${(result.driftFindings ?? []).length} cross-file contradictions detected.`));
|
|
2107
2203
|
parts.push(para(""));
|
|
@@ -2132,51 +2228,80 @@ function buildDocumentXml(result) {
|
|
|
2132
2228
|
}
|
|
2133
2229
|
parts.push(hr());
|
|
2134
2230
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2231
|
+
return parts.join("\n ");
|
|
2232
|
+
}
|
|
2233
|
+
function docxDnaDuplicates(dna) {
|
|
2234
|
+
if (!dna.duplicateGroups?.length) return [];
|
|
2235
|
+
const parts = [];
|
|
2236
|
+
parts.push(para("Semantic Fingerprint Duplicates", "Heading2"));
|
|
2237
|
+
for (const g of dna.duplicateGroups) {
|
|
2238
|
+
const fns = g.functions.map((f) => `${f.name}() in ${f.relativePath || f.file}`).join(", ");
|
|
2239
|
+
parts.push(para(` Group: ${fns}`));
|
|
2240
|
+
}
|
|
2241
|
+
parts.push(para(""));
|
|
2242
|
+
return parts;
|
|
2243
|
+
}
|
|
2244
|
+
function docxDnaSequences(dna) {
|
|
2245
|
+
if (!dna.sequenceSimilarities?.length) return [];
|
|
2246
|
+
const parts = [];
|
|
2247
|
+
parts.push(para("Operation Sequence Matches", "Heading2"));
|
|
2248
|
+
for (const s of dna.sequenceSimilarities) {
|
|
2249
|
+
parts.push(para(` ${Math.round(s.similarity * 100)}% match: ${s.functionA.name}() in ${s.functionA.relativePath || s.functionA.file} \u2194 ${s.functionB.name}() in ${s.functionB.relativePath || s.functionB.file}`));
|
|
2250
|
+
}
|
|
2251
|
+
parts.push(para(""));
|
|
2252
|
+
return parts;
|
|
2253
|
+
}
|
|
2254
|
+
function docxDnaTaintFlows(dna) {
|
|
2255
|
+
if (!dna.taintFlows?.length) return [];
|
|
2256
|
+
const parts = [];
|
|
2257
|
+
parts.push(para("Taint Flows", "Heading2"));
|
|
2258
|
+
for (const t of dna.taintFlows) {
|
|
2259
|
+
parts.push(para(` [${t.sink.severity.toUpperCase()}] ${t.functionName}() in ${t.relativePath || t.file}: ${t.source.type} (line ${t.source.line}) \u2192 ${t.sink.type} (line ${t.sink.line}) \u2014 ${t.sanitized ? "SANITIZED" : "UNSANITIZED"}`));
|
|
2260
|
+
}
|
|
2261
|
+
parts.push(para(""));
|
|
2262
|
+
return parts;
|
|
2263
|
+
}
|
|
2264
|
+
function docxDnaDeviations(dna) {
|
|
2265
|
+
if (!dna.deviationJustifications?.length) return [];
|
|
2266
|
+
const parts = [];
|
|
2267
|
+
parts.push(para("Deviation Justification Analysis", "Heading2"));
|
|
2268
|
+
for (const dj of dna.deviationJustifications) {
|
|
2269
|
+
const verdict = dj.verdict === "likely_justified" ? "JUSTIFIED" : dj.verdict === "likely_accidental" ? "ACCIDENTAL" : "UNCERTAIN";
|
|
2270
|
+
parts.push(para(` [${verdict}] ${dj.relativePath || dj.file}: uses ${dj.deviatingPattern} vs project ${dj.dominantPattern} (score: ${Math.round(dj.justificationScore * 100)}%)`));
|
|
2271
|
+
}
|
|
2272
|
+
parts.push(para(""));
|
|
2273
|
+
return parts;
|
|
2274
|
+
}
|
|
2275
|
+
function docxDnaPatterns(dna) {
|
|
2276
|
+
if (!dna.patternDistributions?.length) return [];
|
|
2277
|
+
const parts = [];
|
|
2278
|
+
parts.push(para("Pattern Classification", "Heading2"));
|
|
2279
|
+
for (const pd of dna.patternDistributions) {
|
|
2280
|
+
const mixed = pd.isInternallyInconsistent ? " [MIXED]" : "";
|
|
2281
|
+
parts.push(para(` ${pd.relativePath || pd.file}: ${pd.dominantPattern} (${Math.round(pd.confidence * 100)}% confidence)${mixed}`));
|
|
2179
2282
|
}
|
|
2283
|
+
parts.push(para(""));
|
|
2284
|
+
return parts;
|
|
2285
|
+
}
|
|
2286
|
+
function buildDocxCodeDnaSection(result) {
|
|
2287
|
+
const dna = result.codeDnaResult;
|
|
2288
|
+
if (!dna) return "";
|
|
2289
|
+
const parts = [];
|
|
2290
|
+
parts.push(para("4. CODE DNA ANALYSIS", "Heading1"));
|
|
2291
|
+
parts.push(para(`${dna.functions?.length ?? 0} functions analyzed in ${dna.timings?.totalMs ?? 0}ms. ${dna.findings?.length ?? 0} findings.`));
|
|
2292
|
+
parts.push(para(""));
|
|
2293
|
+
parts.push(
|
|
2294
|
+
...docxDnaDuplicates(dna),
|
|
2295
|
+
...docxDnaSequences(dna),
|
|
2296
|
+
...docxDnaTaintFlows(dna),
|
|
2297
|
+
...docxDnaDeviations(dna),
|
|
2298
|
+
...docxDnaPatterns(dna)
|
|
2299
|
+
);
|
|
2300
|
+
parts.push(hr());
|
|
2301
|
+
return parts.join("\n ");
|
|
2302
|
+
}
|
|
2303
|
+
function buildDocxPerFileTable(result) {
|
|
2304
|
+
const parts = [];
|
|
2180
2305
|
parts.push(para("5. FILE RANKING", "Heading1"));
|
|
2181
2306
|
const fileSorted = [...result.perFileScores.entries()].sort((a, b) => a[1].score - b[1].score);
|
|
2182
2307
|
parts.push(para(`${fileSorted.length} files scanned. Ranked worst to best.`));
|
|
@@ -2197,8 +2322,12 @@ function buildDocumentXml(result) {
|
|
|
2197
2322
|
tableCellSimple(String(staticCount))
|
|
2198
2323
|
]);
|
|
2199
2324
|
}).join("");
|
|
2200
|
-
parts.push(
|
|
2325
|
+
parts.push(wrapTable(fileHeaderRow + fileDataRows));
|
|
2201
2326
|
parts.push(hr());
|
|
2327
|
+
return parts.join("\n ");
|
|
2328
|
+
}
|
|
2329
|
+
function buildDocxFindingsTable(result) {
|
|
2330
|
+
const parts = [];
|
|
2202
2331
|
parts.push(para("6. STATIC ANALYSIS FINDINGS", "Heading1"));
|
|
2203
2332
|
const statics = result.findings.filter((f) => !f.tags?.includes("drift") && !f.tags?.includes("codedna"));
|
|
2204
2333
|
parts.push(para(`${statics.length} findings from ${13} static analyzers.`));
|
|
@@ -2211,28 +2340,49 @@ function buildDocumentXml(result) {
|
|
|
2211
2340
|
}
|
|
2212
2341
|
if (statics.length > 50) parts.push(para(`... and ${statics.length - 50} more findings.`));
|
|
2213
2342
|
parts.push(hr());
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2343
|
+
return parts.join("\n ");
|
|
2344
|
+
}
|
|
2345
|
+
function buildDocxDeepInsights(result) {
|
|
2346
|
+
if ((result.deepInsights ?? []).length === 0) return "";
|
|
2347
|
+
const parts = [];
|
|
2348
|
+
parts.push(para("7. DEEP ANALYSIS INSIGHTS (AI-POWERED)", "Heading1"));
|
|
2349
|
+
for (const ins of result.deepInsights) {
|
|
2350
|
+
const sevStr = ins.severity === "error" ? "CRITICAL" : ins.severity === "warning" ? "WARNING" : "INFO";
|
|
2351
|
+
parts.push(para(`[${sevStr}] ${ins.title}`, "Heading2"));
|
|
2352
|
+
parts.push(para(ins.description));
|
|
2353
|
+
if (ins.relatedFiles.length > 0) parts.push(para(` Files: ${ins.relatedFiles.join(", ")}`));
|
|
2354
|
+
if (ins.recommendation) {
|
|
2355
|
+
parts.push(boldPara("Recommendation:", "00D4FF"));
|
|
2356
|
+
parts.push(para(` ${ins.recommendation}`));
|
|
2226
2357
|
}
|
|
2227
|
-
parts.push(
|
|
2358
|
+
parts.push(para(""));
|
|
2228
2359
|
}
|
|
2360
|
+
parts.push(hr());
|
|
2361
|
+
return parts.join("\n ");
|
|
2362
|
+
}
|
|
2363
|
+
function buildDocxFooter(result) {
|
|
2364
|
+
const parts = [];
|
|
2229
2365
|
parts.push(para(""));
|
|
2230
2366
|
parts.push(para(`Generated by VibeDrift v${getVersion()} | ${result.context.files.length} files | ${result.context.totalLines.toLocaleString()} lines | ${(result.scanTimeMs / 1e3).toFixed(1)}s | No data sent externally`));
|
|
2231
2367
|
parts.push(para("Re-scan: npx vibedrift ."));
|
|
2368
|
+
return parts.join("\n ");
|
|
2369
|
+
}
|
|
2370
|
+
function buildDocumentXml(result) {
|
|
2371
|
+
const sections = [
|
|
2372
|
+
buildDocxTitlePage(result),
|
|
2373
|
+
buildDocxScoreTable(result),
|
|
2374
|
+
buildDocxIntentSection(result),
|
|
2375
|
+
buildDocxDriftSection(result),
|
|
2376
|
+
buildDocxCodeDnaSection(result),
|
|
2377
|
+
buildDocxPerFileTable(result),
|
|
2378
|
+
buildDocxFindingsTable(result),
|
|
2379
|
+
buildDocxDeepInsights(result),
|
|
2380
|
+
buildDocxFooter(result)
|
|
2381
|
+
].filter(Boolean);
|
|
2232
2382
|
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
2233
2383
|
<w:document xmlns:w="${W}">
|
|
2234
2384
|
<w:body>
|
|
2235
|
-
${
|
|
2385
|
+
${sections.join("\n ")}
|
|
2236
2386
|
<w:sectPr>
|
|
2237
2387
|
<w:pgSz w:w="12240" w:h="15840"/>
|
|
2238
2388
|
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/>
|
|
@@ -2250,13 +2400,14 @@ function renderDocxReport(result) {
|
|
|
2250
2400
|
];
|
|
2251
2401
|
return buildZip(entries);
|
|
2252
2402
|
}
|
|
2253
|
-
var W;
|
|
2403
|
+
var W, TABLE_BORDERS;
|
|
2254
2404
|
var init_docx = __esm({
|
|
2255
2405
|
"src/output/docx.ts"() {
|
|
2256
2406
|
"use strict";
|
|
2257
2407
|
init_esm_shims();
|
|
2258
2408
|
init_version();
|
|
2259
2409
|
W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
2410
|
+
TABLE_BORDERS = `<w:tblBorders><w:top w:val="single" w:sz="4" w:color="CCCCCC"/><w:bottom w:val="single" w:sz="4" w:color="CCCCCC"/><w:insideH w:val="single" w:sz="4" w:color="E0E0E0"/><w:insideV w:val="single" w:sz="4" w:color="E0E0E0"/></w:tblBorders>`;
|
|
2260
2411
|
}
|
|
2261
2412
|
});
|
|
2262
2413
|
|
|
@@ -2985,88 +3136,100 @@ var dependenciesAnalyzer = {
|
|
|
2985
3136
|
return findings;
|
|
2986
3137
|
}
|
|
2987
3138
|
};
|
|
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;
|
|
3139
|
+
function collectImportedPackages(files) {
|
|
3004
3140
|
const imported = /* @__PURE__ */ new Set();
|
|
3005
3141
|
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) {
|
|
3142
|
+
for (const file of files) {
|
|
3010
3143
|
for (const pattern of JS_IMPORT_PATTERNS) {
|
|
3011
3144
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
3012
3145
|
let match;
|
|
3013
3146
|
while ((match = regex.exec(file.content)) !== null) {
|
|
3014
|
-
const
|
|
3015
|
-
if (NODE_BUILTINS.has(
|
|
3016
|
-
imported.add(
|
|
3017
|
-
if (!importLocations.has(
|
|
3018
|
-
importLocations.get(
|
|
3147
|
+
const pkg = extractJsPackageName(match[1]);
|
|
3148
|
+
if (NODE_BUILTINS.has(pkg) || pkg.startsWith("node:") || pkg.startsWith("@/") || pkg.startsWith("~")) continue;
|
|
3149
|
+
imported.add(pkg);
|
|
3150
|
+
if (!importLocations.has(pkg)) importLocations.set(pkg, []);
|
|
3151
|
+
importLocations.get(pkg).push({
|
|
3019
3152
|
file: file.relativePath,
|
|
3020
3153
|
line: file.content.slice(0, match.index).split("\n").length
|
|
3021
3154
|
});
|
|
3022
3155
|
}
|
|
3023
3156
|
}
|
|
3024
3157
|
}
|
|
3158
|
+
return { imported, importLocations };
|
|
3159
|
+
}
|
|
3160
|
+
function detectPhantomDeps(declared, imported, devToolPatterns) {
|
|
3025
3161
|
const phantom = [...declared].filter((d) => !imported.has(d));
|
|
3026
3162
|
const realPhantom = phantom.filter(
|
|
3027
|
-
(p) => !
|
|
3163
|
+
(p) => !devToolPatterns.some((pat) => p.includes(pat))
|
|
3028
3164
|
);
|
|
3029
3165
|
if (realPhantom.length > 0) {
|
|
3030
|
-
|
|
3166
|
+
return [{
|
|
3031
3167
|
analyzerId: "dependencies",
|
|
3032
3168
|
severity: realPhantom.length > 5 ? "error" : "warning",
|
|
3033
3169
|
confidence: 0.75,
|
|
3034
3170
|
message: `${realPhantom.length} phantom dependencies (declared but unused): ${realPhantom.slice(0, 5).join(", ")}${realPhantom.length > 5 ? "..." : ""}`,
|
|
3035
3171
|
locations: realPhantom.map((p) => ({ file: "package.json" })),
|
|
3036
3172
|
tags: ["deps", "phantom", "js"]
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3173
|
+
}];
|
|
3174
|
+
}
|
|
3175
|
+
return [];
|
|
3176
|
+
}
|
|
3177
|
+
var ALIAS_PATTERNS = [
|
|
3178
|
+
/^#/,
|
|
3179
|
+
/^virtual:/,
|
|
3180
|
+
/^vite\//,
|
|
3181
|
+
/^next\//,
|
|
3182
|
+
/^\$/,
|
|
3183
|
+
/^server-only$/,
|
|
3184
|
+
/^client-only$/
|
|
3185
|
+
];
|
|
3186
|
+
function detectMissingDeps(declared, imported, isMonorepo, importLocations) {
|
|
3048
3187
|
const missing = [...imported].filter((i) => {
|
|
3049
3188
|
if (declared.has(i)) return false;
|
|
3050
3189
|
if (ALIAS_PATTERNS.some((p) => p.test(i))) return false;
|
|
3051
3190
|
return true;
|
|
3052
3191
|
});
|
|
3053
|
-
const missingConfidence = isMonorepo
|
|
3054
|
-
const missingSeverity = isMonorepo
|
|
3192
|
+
const missingConfidence = isMonorepo ? 0.4 : 0.75;
|
|
3193
|
+
const missingSeverity = isMonorepo ? "warning" : "error";
|
|
3055
3194
|
if (missing.length > 0) {
|
|
3056
|
-
|
|
3195
|
+
return [{
|
|
3057
3196
|
analyzerId: "dependencies",
|
|
3058
3197
|
severity: missingSeverity,
|
|
3059
3198
|
confidence: missingConfidence,
|
|
3060
|
-
message: `${missing.length} packages imported but not in package.json: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? "..." : ""}${isMonorepo
|
|
3199
|
+
message: `${missing.length} packages imported but not in package.json: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? "..." : ""}${isMonorepo ? " (may be workspace packages)" : ""}`,
|
|
3061
3200
|
locations: missing.slice(0, 5).flatMap((p) => importLocations.get(p) ?? []),
|
|
3062
3201
|
tags: ["deps", "missing", "js"]
|
|
3063
|
-
}
|
|
3202
|
+
}];
|
|
3064
3203
|
}
|
|
3065
|
-
return
|
|
3204
|
+
return [];
|
|
3066
3205
|
}
|
|
3067
|
-
function
|
|
3206
|
+
function analyzeJsDeps(ctx) {
|
|
3068
3207
|
const findings = [];
|
|
3069
|
-
const
|
|
3208
|
+
const pkg = ctx.packageJson;
|
|
3209
|
+
const declared = /* @__PURE__ */ new Set([
|
|
3210
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
3211
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
3212
|
+
...Object.keys(pkg.peerDependencies ?? {}),
|
|
3213
|
+
...Object.keys(pkg.optionalDependencies ?? {})
|
|
3214
|
+
]);
|
|
3215
|
+
const isMonorepo = [
|
|
3216
|
+
...Object.values(pkg.dependencies ?? {}),
|
|
3217
|
+
...Object.values(pkg.devDependencies ?? {})
|
|
3218
|
+
].some(
|
|
3219
|
+
(v) => typeof v === "string" && (v.startsWith("workspace:") || v === "*")
|
|
3220
|
+
);
|
|
3221
|
+
const hasWorkspaces = !!pkg.workspaces;
|
|
3222
|
+
const jsFiles = ctx.files.filter(
|
|
3223
|
+
(f) => (f.language === "javascript" || f.language === "typescript") && !FIXTURE_PATH_PATTERN.test(f.relativePath)
|
|
3224
|
+
);
|
|
3225
|
+
const { imported, importLocations } = collectImportedPackages(jsFiles);
|
|
3226
|
+
findings.push(...detectPhantomDeps(declared, imported, DEV_TOOL_PATTERNS));
|
|
3227
|
+
findings.push(...detectMissingDeps(declared, imported, isMonorepo || hasWorkspaces, importLocations));
|
|
3228
|
+
return findings;
|
|
3229
|
+
}
|
|
3230
|
+
function analyzeGoDeps(ctx) {
|
|
3231
|
+
const findings = [];
|
|
3232
|
+
const goMod = ctx.goMod;
|
|
3070
3233
|
const declaredPaths = new Set(goMod.require.map((r) => r.path));
|
|
3071
3234
|
const importedPaths = /* @__PURE__ */ new Set();
|
|
3072
3235
|
const goFiles = ctx.files.filter((f) => f.language === "go");
|
|
@@ -4392,43 +4555,24 @@ function detectLongFunctions(ctx) {
|
|
|
4392
4555
|
}
|
|
4393
4556
|
return findings;
|
|
4394
4557
|
}
|
|
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
|
-
});
|
|
4558
|
+
function countFileCommentDensity(file) {
|
|
4559
|
+
const lines = file.content.split("\n");
|
|
4560
|
+
let commentLines = 0;
|
|
4561
|
+
for (const line of lines) {
|
|
4562
|
+
const trimmed = line.trim();
|
|
4563
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("///") || trimmed.startsWith('"""')) {
|
|
4564
|
+
commentLines++;
|
|
4415
4565
|
}
|
|
4416
4566
|
}
|
|
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
|
-
}
|
|
4567
|
+
return {
|
|
4568
|
+
lines: file.lineCount,
|
|
4569
|
+
commentLines,
|
|
4570
|
+
density: commentLines / file.lineCount
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
function findUndocumentedExports(files) {
|
|
4430
4574
|
const undocumentedExports = [];
|
|
4431
|
-
for (const file of
|
|
4575
|
+
for (const file of files) {
|
|
4432
4576
|
if (!file.language) continue;
|
|
4433
4577
|
const lines = file.content.split("\n");
|
|
4434
4578
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -4452,6 +4596,36 @@ function detectLowDocumentation(ctx) {
|
|
|
4452
4596
|
}
|
|
4453
4597
|
}
|
|
4454
4598
|
}
|
|
4599
|
+
return undocumentedExports;
|
|
4600
|
+
}
|
|
4601
|
+
function detectLowDocumentation(ctx) {
|
|
4602
|
+
const findings = [];
|
|
4603
|
+
const underdocumented = [];
|
|
4604
|
+
for (const file of ctx.files) {
|
|
4605
|
+
if (file.lineCount < 100) continue;
|
|
4606
|
+
const { density } = countFileCommentDensity(file);
|
|
4607
|
+
if (density < 0.05) {
|
|
4608
|
+
underdocumented.push({
|
|
4609
|
+
file: file.relativePath,
|
|
4610
|
+
lines: file.lineCount,
|
|
4611
|
+
commentRatio: Math.round(density * 100)
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
if (underdocumented.length > 2) {
|
|
4616
|
+
findings.push({
|
|
4617
|
+
analyzerId: "intent-clarity",
|
|
4618
|
+
severity: underdocumented.length > 5 ? "warning" : "info",
|
|
4619
|
+
confidence: 0.6,
|
|
4620
|
+
message: `${underdocumented.length} files over 100 lines have <5% comment density \u2014 intent may be unclear to maintainers`,
|
|
4621
|
+
locations: underdocumented.slice(0, 10).map((f) => ({
|
|
4622
|
+
file: f.file,
|
|
4623
|
+
snippet: `${f.lines} lines, ${f.commentRatio}% comments`
|
|
4624
|
+
})),
|
|
4625
|
+
tags: ["intent", "documentation"]
|
|
4626
|
+
});
|
|
4627
|
+
}
|
|
4628
|
+
const undocumentedExports = findUndocumentedExports(ctx.files);
|
|
4455
4629
|
if (undocumentedExports.length > 10) {
|
|
4456
4630
|
const ratio = ctx.files.length > 0 ? Math.round(undocumentedExports.length / ctx.files.length * 10) / 10 : 0;
|
|
4457
4631
|
findings.push({
|
|
@@ -4538,8 +4712,7 @@ var deadCodeAnalyzer = {
|
|
|
4538
4712
|
return findings;
|
|
4539
4713
|
}
|
|
4540
4714
|
};
|
|
4541
|
-
function
|
|
4542
|
-
const findings = [];
|
|
4715
|
+
function buildExportMap(files) {
|
|
4543
4716
|
const exports = [];
|
|
4544
4717
|
for (const file of files) {
|
|
4545
4718
|
if (isEntryPoint(file.relativePath)) continue;
|
|
@@ -4586,6 +4759,9 @@ function analyzeJsDeadExports(files) {
|
|
|
4586
4759
|
}
|
|
4587
4760
|
}
|
|
4588
4761
|
}
|
|
4762
|
+
return exports;
|
|
4763
|
+
}
|
|
4764
|
+
function buildImportSet(files) {
|
|
4589
4765
|
const importedNames = /* @__PURE__ */ new Set();
|
|
4590
4766
|
for (const file of files) {
|
|
4591
4767
|
for (const pattern of IMPORT_PATTERNS_JS) {
|
|
@@ -4604,10 +4780,19 @@ function analyzeJsDeadExports(files) {
|
|
|
4604
4780
|
}
|
|
4605
4781
|
}
|
|
4606
4782
|
}
|
|
4607
|
-
|
|
4608
|
-
|
|
4783
|
+
return importedNames;
|
|
4784
|
+
}
|
|
4785
|
+
function identifyDeadExports(exports, importedNames, allContent) {
|
|
4786
|
+
return exports.filter(
|
|
4609
4787
|
(e) => !importedNames.has(e.name) && countOccurrences(allContent, e.name) <= 1
|
|
4610
4788
|
);
|
|
4789
|
+
}
|
|
4790
|
+
function analyzeJsDeadExports(files) {
|
|
4791
|
+
const findings = [];
|
|
4792
|
+
const exports = buildExportMap(files);
|
|
4793
|
+
const importedNames = buildImportSet(files);
|
|
4794
|
+
const allContent = files.map((f) => f.content).join("\n");
|
|
4795
|
+
const deadExports = identifyDeadExports(exports, importedNames, allContent);
|
|
4611
4796
|
if (deadExports.length > 3) {
|
|
4612
4797
|
findings.push({
|
|
4613
4798
|
analyzerId: "dead-code",
|
|
@@ -4749,19 +4934,13 @@ var languageSpecificAnalyzer = {
|
|
|
4749
4934
|
return findings;
|
|
4750
4935
|
}
|
|
4751
4936
|
};
|
|
4752
|
-
function
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
const uncheckedLocations = [];
|
|
4756
|
-
let nakedGoroutines = 0;
|
|
4757
|
-
const goroutineLocations = [];
|
|
4758
|
-
let unsafeMutex = 0;
|
|
4759
|
-
const mutexLocations = [];
|
|
4937
|
+
function detectGoUncheckedErrors(files) {
|
|
4938
|
+
let count = 0;
|
|
4939
|
+
const locations = [];
|
|
4760
4940
|
for (const file of files) {
|
|
4761
4941
|
const lines = file.content.split("\n");
|
|
4762
4942
|
for (let i = 0; i < lines.length; i++) {
|
|
4763
|
-
const
|
|
4764
|
-
const trimmed = line.trim();
|
|
4943
|
+
const trimmed = lines[i].trim();
|
|
4765
4944
|
if (/\berr\s*[:=]/.test(trimmed) && !trimmed.startsWith("//")) {
|
|
4766
4945
|
let nextLine = "";
|
|
4767
4946
|
for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
|
|
@@ -4772,57 +4951,86 @@ function analyzeGo(files) {
|
|
|
4772
4951
|
}
|
|
4773
4952
|
}
|
|
4774
4953
|
if (nextLine && !nextLine.includes("err") && !nextLine.startsWith("return")) {
|
|
4775
|
-
|
|
4776
|
-
|
|
4954
|
+
count++;
|
|
4955
|
+
locations.push({
|
|
4777
4956
|
file: file.relativePath,
|
|
4778
4957
|
line: i + 1,
|
|
4779
4958
|
snippet: trimmed.slice(0, 80)
|
|
4780
4959
|
});
|
|
4781
4960
|
}
|
|
4782
4961
|
}
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
return { count, locations };
|
|
4965
|
+
}
|
|
4966
|
+
function detectGoNakedGoroutines(files) {
|
|
4967
|
+
let count = 0;
|
|
4968
|
+
const locations = [];
|
|
4969
|
+
for (const file of files) {
|
|
4970
|
+
const lines = file.content.split("\n");
|
|
4971
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4972
|
+
const line = lines[i];
|
|
4783
4973
|
if (/^\s*go\s+func\s*\(/.test(line) || /^\s*go\s+\w+\s*\(/.test(line)) {
|
|
4784
4974
|
const nearby = lines.slice(Math.max(0, i - 2), i + 3).join(" ");
|
|
4785
4975
|
if (!/\bctx\b/.test(nearby) && !/context\./.test(nearby)) {
|
|
4786
|
-
|
|
4787
|
-
|
|
4976
|
+
count++;
|
|
4977
|
+
locations.push({ file: file.relativePath, line: i + 1 });
|
|
4788
4978
|
}
|
|
4789
4979
|
}
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
return { count, locations };
|
|
4983
|
+
}
|
|
4984
|
+
function detectGoUnsafeMutex(files) {
|
|
4985
|
+
let count = 0;
|
|
4986
|
+
const locations = [];
|
|
4987
|
+
for (const file of files) {
|
|
4988
|
+
const lines = file.content.split("\n");
|
|
4989
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4990
|
+
const trimmed = lines[i].trim();
|
|
4790
4991
|
if (/\.Lock\(\)/.test(trimmed)) {
|
|
4791
4992
|
const nextLines = lines.slice(i + 1, i + 4).join(" ");
|
|
4792
4993
|
if (!/defer\s+.*\.Unlock\(\)/.test(nextLines) && !/\.Unlock\(\)/.test(trimmed)) {
|
|
4793
|
-
|
|
4794
|
-
|
|
4994
|
+
count++;
|
|
4995
|
+
locations.push({ file: file.relativePath, line: i + 1 });
|
|
4795
4996
|
}
|
|
4796
4997
|
}
|
|
4797
4998
|
}
|
|
4798
4999
|
}
|
|
4799
|
-
|
|
5000
|
+
return { count, locations };
|
|
5001
|
+
}
|
|
5002
|
+
function analyzeGo(files) {
|
|
5003
|
+
const findings = [];
|
|
5004
|
+
const uncheckedErrors = detectGoUncheckedErrors(files);
|
|
5005
|
+
const nakedGoroutines = detectGoNakedGoroutines(files);
|
|
5006
|
+
const unsafeMutex = detectGoUnsafeMutex(files);
|
|
5007
|
+
if (uncheckedErrors.count > 0) {
|
|
4800
5008
|
findings.push({
|
|
4801
5009
|
analyzerId: "language-specific",
|
|
4802
|
-
severity: uncheckedErrors > 10 ? "error" : "warning",
|
|
5010
|
+
severity: uncheckedErrors.count > 10 ? "error" : "warning",
|
|
4803
5011
|
confidence: 0.7,
|
|
4804
|
-
message: `${uncheckedErrors} potentially unchecked errors in Go code`,
|
|
4805
|
-
locations:
|
|
5012
|
+
message: `${uncheckedErrors.count} potentially unchecked errors in Go code`,
|
|
5013
|
+
locations: uncheckedErrors.locations.slice(0, 10),
|
|
4806
5014
|
tags: ["go", "error-handling", "unchecked-error"]
|
|
4807
5015
|
});
|
|
4808
5016
|
}
|
|
4809
|
-
if (nakedGoroutines > 0) {
|
|
5017
|
+
if (nakedGoroutines.count > 0) {
|
|
4810
5018
|
findings.push({
|
|
4811
5019
|
analyzerId: "language-specific",
|
|
4812
5020
|
severity: "warning",
|
|
4813
5021
|
confidence: 0.6,
|
|
4814
|
-
message: `${nakedGoroutines} goroutines launched without context \u2014 potential leak risk`,
|
|
4815
|
-
locations:
|
|
5022
|
+
message: `${nakedGoroutines.count} goroutines launched without context \u2014 potential leak risk`,
|
|
5023
|
+
locations: nakedGoroutines.locations.slice(0, 10),
|
|
4816
5024
|
tags: ["go", "goroutine", "leak"]
|
|
4817
5025
|
});
|
|
4818
5026
|
}
|
|
4819
|
-
if (unsafeMutex > 0) {
|
|
5027
|
+
if (unsafeMutex.count > 0) {
|
|
4820
5028
|
findings.push({
|
|
4821
5029
|
analyzerId: "language-specific",
|
|
4822
5030
|
severity: "warning",
|
|
4823
5031
|
confidence: 0.75,
|
|
4824
|
-
message: `${unsafeMutex} mutex locks without defer Unlock \u2014 risk of deadlock`,
|
|
4825
|
-
locations:
|
|
5032
|
+
message: `${unsafeMutex.count} mutex locks without defer Unlock \u2014 risk of deadlock`,
|
|
5033
|
+
locations: unsafeMutex.locations.slice(0, 10),
|
|
4826
5034
|
tags: ["go", "mutex", "concurrency"]
|
|
4827
5035
|
});
|
|
4828
5036
|
}
|
|
@@ -5069,39 +5277,34 @@ function buildProfile(file) {
|
|
|
5069
5277
|
if (dataAccess.length === 0 && errorHandling.length === 0) return null;
|
|
5070
5278
|
return { file: file.path, language: file.language, dataAccess, errorHandling, config, di };
|
|
5071
5279
|
}
|
|
5072
|
-
function
|
|
5280
|
+
function detectFilePattern(patterns) {
|
|
5281
|
+
const fileCounts = /* @__PURE__ */ new Map();
|
|
5282
|
+
for (const { pattern } of patterns) {
|
|
5283
|
+
fileCounts.set(pattern, (fileCounts.get(pattern) ?? 0) + 1);
|
|
5284
|
+
}
|
|
5285
|
+
let primaryPattern = null;
|
|
5286
|
+
let maxCount = 0;
|
|
5287
|
+
for (const [pat, count] of fileCounts) {
|
|
5288
|
+
if (count > maxCount) {
|
|
5289
|
+
maxCount = count;
|
|
5290
|
+
primaryPattern = pat;
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5293
|
+
return primaryPattern;
|
|
5294
|
+
}
|
|
5295
|
+
function buildPatternDistribution(profiles) {
|
|
5073
5296
|
const counts = /* @__PURE__ */ new Map();
|
|
5074
5297
|
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
|
-
}
|
|
5298
|
+
const primaryPattern = detectFilePattern(p.patterns);
|
|
5087
5299
|
if (!primaryPattern) continue;
|
|
5088
5300
|
if (!counts.has(primaryPattern)) counts.set(primaryPattern, { count: 0, files: [] });
|
|
5089
5301
|
const entry = counts.get(primaryPattern);
|
|
5090
5302
|
entry.count++;
|
|
5091
5303
|
entry.files.push(p.file);
|
|
5092
5304
|
}
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
for (const [pattern, data] of counts) {
|
|
5097
|
-
if (data.count > dominantCount) {
|
|
5098
|
-
dominantCount = data.count;
|
|
5099
|
-
dominant = pattern;
|
|
5100
|
-
}
|
|
5101
|
-
}
|
|
5102
|
-
if (!dominant) return null;
|
|
5103
|
-
const totalFiles = profiles.length;
|
|
5104
|
-
const consistencyScore = Math.round(dominantCount / totalFiles * 100);
|
|
5305
|
+
return counts;
|
|
5306
|
+
}
|
|
5307
|
+
function collectDeviatingFiles(counts, dominant, profiles, patternNames) {
|
|
5105
5308
|
const deviating = [];
|
|
5106
5309
|
for (const [pattern, data] of counts) {
|
|
5107
5310
|
if (pattern === dominant) continue;
|
|
@@ -5115,6 +5318,29 @@ function analyzePatternDistribution(profiles, patternNames) {
|
|
|
5115
5318
|
});
|
|
5116
5319
|
}
|
|
5117
5320
|
}
|
|
5321
|
+
return deviating;
|
|
5322
|
+
}
|
|
5323
|
+
function findDominantPattern(counts) {
|
|
5324
|
+
let dominant = null;
|
|
5325
|
+
let dominantCount = 0;
|
|
5326
|
+
for (const [pattern, data] of counts) {
|
|
5327
|
+
if (data.count > dominantCount) {
|
|
5328
|
+
dominantCount = data.count;
|
|
5329
|
+
dominant = pattern;
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
if (!dominant) return null;
|
|
5333
|
+
return { dominant, dominantCount };
|
|
5334
|
+
}
|
|
5335
|
+
function analyzePatternDistribution(profiles, patternNames) {
|
|
5336
|
+
const counts = buildPatternDistribution(profiles);
|
|
5337
|
+
if (counts.size < 2) return null;
|
|
5338
|
+
const result = findDominantPattern(counts);
|
|
5339
|
+
if (!result) return null;
|
|
5340
|
+
const { dominant, dominantCount } = result;
|
|
5341
|
+
const totalFiles = profiles.length;
|
|
5342
|
+
const consistencyScore = Math.round(dominantCount / totalFiles * 100);
|
|
5343
|
+
const deviating = collectDeviatingFiles(counts, dominant, profiles, patternNames);
|
|
5118
5344
|
if (deviating.length === 0) return null;
|
|
5119
5345
|
return {
|
|
5120
5346
|
detector: "architectural_consistency",
|
|
@@ -5216,13 +5442,25 @@ function classifyName(name) {
|
|
|
5216
5442
|
if (/^[a-z][a-z0-9]*$/.test(name)) return "camelCase";
|
|
5217
5443
|
return null;
|
|
5218
5444
|
}
|
|
5445
|
+
function isIdiomaticGo(name, convention) {
|
|
5446
|
+
if (convention === "PascalCase" && /^[A-Z]/.test(name)) return true;
|
|
5447
|
+
if (/^(?:HTTP|URL|ID|JSON|XML|SQL|API|DNS|TCP|UDP|IP|TLS|SSH|EOF)/.test(name)) return true;
|
|
5448
|
+
return false;
|
|
5449
|
+
}
|
|
5450
|
+
function isIdiomaticJsTs(name, convention, symbolType) {
|
|
5451
|
+
if (symbolType === "class" && convention === "PascalCase") return true;
|
|
5452
|
+
if (symbolType === "function" && convention === "PascalCase" && /^[A-Z]\w*$/.test(name)) return true;
|
|
5453
|
+
return false;
|
|
5454
|
+
}
|
|
5455
|
+
function isIdiomaticPython(name, convention, symbolType) {
|
|
5456
|
+
if (symbolType === "class" && convention === "PascalCase") return true;
|
|
5457
|
+
if (name.startsWith("__") && name.endsWith("__")) return true;
|
|
5458
|
+
return false;
|
|
5459
|
+
}
|
|
5219
5460
|
function isIdiomatic(name, convention, symbolType, language) {
|
|
5220
|
-
if (language === "go" &&
|
|
5221
|
-
if (language === "
|
|
5222
|
-
if (
|
|
5223
|
-
if ((language === "javascript" || language === "typescript") && symbolType === "function" && convention === "PascalCase" && /^[A-Z]\w*$/.test(name)) return true;
|
|
5224
|
-
if (language === "python" && symbolType === "class" && convention === "PascalCase") return true;
|
|
5225
|
-
if (language === "python" && name.startsWith("__") && name.endsWith("__")) return true;
|
|
5461
|
+
if (language === "go" && isIdiomaticGo(name, convention)) return true;
|
|
5462
|
+
if ((language === "javascript" || language === "typescript") && isIdiomaticJsTs(name, convention, symbolType)) return true;
|
|
5463
|
+
if (language === "python" && isIdiomaticPython(name, convention, symbolType)) return true;
|
|
5226
5464
|
if (symbolType === "constant" && convention === "SCREAMING_SNAKE") return true;
|
|
5227
5465
|
return false;
|
|
5228
5466
|
}
|
|
@@ -5260,29 +5498,40 @@ function extractSymbols(file) {
|
|
|
5260
5498
|
}
|
|
5261
5499
|
return symbols;
|
|
5262
5500
|
}
|
|
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;
|
|
5501
|
+
function classifyBaseName(filePath) {
|
|
5502
|
+
const basename2 = filePath.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "";
|
|
5503
|
+
if (basename2.length <= 1) return null;
|
|
5504
|
+
if (/(?:test|spec|config|setup|__)/i.test(basename2)) return null;
|
|
5505
|
+
const convention = classifyName(basename2);
|
|
5506
|
+
if (!convention) return null;
|
|
5507
|
+
return { basename: basename2, convention };
|
|
5508
|
+
}
|
|
5509
|
+
function findDominantConvention(fileNameConventions) {
|
|
5275
5510
|
let dominant = null;
|
|
5276
5511
|
let maxCount = 0;
|
|
5277
5512
|
let totalFiles = 0;
|
|
5278
|
-
for (const [conv,
|
|
5279
|
-
totalFiles +=
|
|
5280
|
-
if (
|
|
5281
|
-
maxCount =
|
|
5513
|
+
for (const [conv, files] of fileNameConventions) {
|
|
5514
|
+
totalFiles += files.length;
|
|
5515
|
+
if (files.length > maxCount) {
|
|
5516
|
+
maxCount = files.length;
|
|
5282
5517
|
dominant = conv;
|
|
5283
5518
|
}
|
|
5284
5519
|
}
|
|
5285
5520
|
if (!dominant || maxCount === totalFiles) return null;
|
|
5521
|
+
return { dominant, maxCount, totalFiles };
|
|
5522
|
+
}
|
|
5523
|
+
function analyzeFileNaming(files) {
|
|
5524
|
+
const fileNameConventions = /* @__PURE__ */ new Map();
|
|
5525
|
+
for (const file of files) {
|
|
5526
|
+
const classified = classifyBaseName(file.path);
|
|
5527
|
+
if (!classified) continue;
|
|
5528
|
+
if (!fileNameConventions.has(classified.convention)) fileNameConventions.set(classified.convention, []);
|
|
5529
|
+
fileNameConventions.get(classified.convention).push(file.path);
|
|
5530
|
+
}
|
|
5531
|
+
if (fileNameConventions.size < 2) return null;
|
|
5532
|
+
const result = findDominantConvention(fileNameConventions);
|
|
5533
|
+
if (!result) return null;
|
|
5534
|
+
const { dominant, maxCount, totalFiles } = result;
|
|
5286
5535
|
const deviating = [];
|
|
5287
5536
|
for (const [conv, filePaths] of fileNameConventions) {
|
|
5288
5537
|
if (conv === dominant) continue;
|
|
@@ -5306,6 +5555,68 @@ function analyzeFileNaming(files) {
|
|
|
5306
5555
|
recommendation: `Standardize file names to ${dominant}. ${deviating.length} files use a different convention.`
|
|
5307
5556
|
};
|
|
5308
5557
|
}
|
|
5558
|
+
function collectDeviantFiles(convCounts, dominant) {
|
|
5559
|
+
const fileDeviants = /* @__PURE__ */ new Map();
|
|
5560
|
+
for (const [conv, syms] of convCounts) {
|
|
5561
|
+
if (conv === dominant) continue;
|
|
5562
|
+
for (const s of syms) {
|
|
5563
|
+
if (!fileDeviants.has(s.file)) fileDeviants.set(s.file, []);
|
|
5564
|
+
fileDeviants.get(s.file).push(s);
|
|
5565
|
+
}
|
|
5566
|
+
}
|
|
5567
|
+
const deviatingFiles = [];
|
|
5568
|
+
for (const [filePath, syms] of fileDeviants) {
|
|
5569
|
+
const uniqueConventions = [...new Set(syms.map((s) => s.convention))];
|
|
5570
|
+
deviatingFiles.push({
|
|
5571
|
+
path: filePath,
|
|
5572
|
+
detectedPattern: uniqueConventions.join(", "),
|
|
5573
|
+
evidence: syms.slice(0, 3).map((s) => ({ line: s.line, code: s.name }))
|
|
5574
|
+
});
|
|
5575
|
+
}
|
|
5576
|
+
return deviatingFiles;
|
|
5577
|
+
}
|
|
5578
|
+
function buildConventionFinding(type, dominant, maxCount, totalSymbols, deviatingFiles) {
|
|
5579
|
+
const deviantCount = totalSymbols - maxCount;
|
|
5580
|
+
const consistencyScore = Math.round(maxCount / totalSymbols * 100);
|
|
5581
|
+
return {
|
|
5582
|
+
detector: "naming_conventions",
|
|
5583
|
+
subCategory: `${type}_names`,
|
|
5584
|
+
driftCategory: "naming_conventions",
|
|
5585
|
+
severity: deviatingFiles.length > 5 ? "error" : "warning",
|
|
5586
|
+
confidence: 0.8,
|
|
5587
|
+
finding: `${type} naming convention oscillates: ${maxCount} use ${dominant}, ${deviantCount} use other conventions \u2014 likely from different AI sessions`,
|
|
5588
|
+
dominantPattern: dominant,
|
|
5589
|
+
dominantCount: maxCount,
|
|
5590
|
+
totalRelevantFiles: totalSymbols,
|
|
5591
|
+
consistencyScore,
|
|
5592
|
+
deviatingFiles: deviatingFiles.slice(0, 10),
|
|
5593
|
+
recommendation: `${maxCount} of ${totalSymbols} ${type} names use ${dominant}. Standardize deviating names.`
|
|
5594
|
+
};
|
|
5595
|
+
}
|
|
5596
|
+
function analyzeSymbolTypeConventions(type, typeSymbols) {
|
|
5597
|
+
if (typeSymbols.length < 3) return null;
|
|
5598
|
+
const convCounts = /* @__PURE__ */ new Map();
|
|
5599
|
+
for (const s of typeSymbols) {
|
|
5600
|
+
if (!convCounts.has(s.convention)) convCounts.set(s.convention, []);
|
|
5601
|
+
convCounts.get(s.convention).push(s);
|
|
5602
|
+
}
|
|
5603
|
+
if (convCounts.size < 2) return null;
|
|
5604
|
+
let dominant = null;
|
|
5605
|
+
let maxCount = 0;
|
|
5606
|
+
for (const [conv, syms] of convCounts) {
|
|
5607
|
+
if (syms.length > maxCount) {
|
|
5608
|
+
maxCount = syms.length;
|
|
5609
|
+
dominant = conv;
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
if (!dominant) return null;
|
|
5613
|
+
const totalSymbols = typeSymbols.length;
|
|
5614
|
+
const deviantCount = totalSymbols - maxCount;
|
|
5615
|
+
if (deviantCount < 3 || deviantCount / totalSymbols < 0.1) return null;
|
|
5616
|
+
const deviatingFiles = collectDeviantFiles(convCounts, dominant);
|
|
5617
|
+
if (deviatingFiles.length < 2) return null;
|
|
5618
|
+
return buildConventionFinding(type, dominant, maxCount, totalSymbols, deviatingFiles);
|
|
5619
|
+
}
|
|
5309
5620
|
var conventionOscillation = {
|
|
5310
5621
|
id: "convention-oscillation",
|
|
5311
5622
|
name: "Naming Convention Oscillation",
|
|
@@ -5320,62 +5631,8 @@ var conventionOscillation = {
|
|
|
5320
5631
|
const symbolTypes = [...new Set(allSymbols.map((s) => s.symbolType))];
|
|
5321
5632
|
for (const type of symbolTypes) {
|
|
5322
5633
|
const typeSymbols = allSymbols.filter((s) => s.symbolType === type);
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
for (const s of typeSymbols) {
|
|
5326
|
-
if (!convCounts.has(s.convention)) convCounts.set(s.convention, []);
|
|
5327
|
-
convCounts.get(s.convention).push(s);
|
|
5328
|
-
}
|
|
5329
|
-
if (convCounts.size < 2) continue;
|
|
5330
|
-
let dominant = null;
|
|
5331
|
-
let maxCount = 0;
|
|
5332
|
-
for (const [conv, syms] of convCounts) {
|
|
5333
|
-
if (syms.length > maxCount) {
|
|
5334
|
-
maxCount = syms.length;
|
|
5335
|
-
dominant = conv;
|
|
5336
|
-
}
|
|
5337
|
-
}
|
|
5338
|
-
if (!dominant) continue;
|
|
5339
|
-
const totalSymbols = typeSymbols.length;
|
|
5340
|
-
const consistencyScore = Math.round(maxCount / totalSymbols * 100);
|
|
5341
|
-
const deviantCount = totalSymbols - maxCount;
|
|
5342
|
-
if (deviantCount < 3 || deviantCount / totalSymbols < 0.1) continue;
|
|
5343
|
-
const fileDeviants = /* @__PURE__ */ new Map();
|
|
5344
|
-
for (const [conv, syms] of convCounts) {
|
|
5345
|
-
if (conv === dominant) continue;
|
|
5346
|
-
for (const s of syms) {
|
|
5347
|
-
if (!fileDeviants.has(s.file)) fileDeviants.set(s.file, []);
|
|
5348
|
-
fileDeviants.get(s.file).push(s);
|
|
5349
|
-
}
|
|
5350
|
-
}
|
|
5351
|
-
const deviatingFiles = [];
|
|
5352
|
-
for (const [filePath, syms] of fileDeviants) {
|
|
5353
|
-
const uniqueConventions = [...new Set(syms.map((s) => s.convention))];
|
|
5354
|
-
deviatingFiles.push({
|
|
5355
|
-
path: filePath,
|
|
5356
|
-
detectedPattern: uniqueConventions.join(", "),
|
|
5357
|
-
evidence: syms.slice(0, 3).map((s) => ({ line: s.line, code: s.name }))
|
|
5358
|
-
});
|
|
5359
|
-
}
|
|
5360
|
-
if (deviatingFiles.length < 2) continue;
|
|
5361
|
-
const distribution = {};
|
|
5362
|
-
for (const [conv, syms] of convCounts) {
|
|
5363
|
-
distribution[conv] = syms.length;
|
|
5364
|
-
}
|
|
5365
|
-
findings.push({
|
|
5366
|
-
detector: "naming_conventions",
|
|
5367
|
-
subCategory: `${type}_names`,
|
|
5368
|
-
driftCategory: "naming_conventions",
|
|
5369
|
-
severity: deviatingFiles.length > 5 ? "error" : "warning",
|
|
5370
|
-
confidence: 0.8,
|
|
5371
|
-
finding: `${type} naming convention oscillates: ${maxCount} use ${dominant}, ${deviantCount} use other conventions \u2014 likely from different AI sessions`,
|
|
5372
|
-
dominantPattern: dominant,
|
|
5373
|
-
dominantCount: maxCount,
|
|
5374
|
-
totalRelevantFiles: totalSymbols,
|
|
5375
|
-
consistencyScore,
|
|
5376
|
-
deviatingFiles: deviatingFiles.slice(0, 10),
|
|
5377
|
-
recommendation: `${maxCount} of ${totalSymbols} ${type} names use ${dominant}. Standardize deviating names.`
|
|
5378
|
-
});
|
|
5634
|
+
const finding = analyzeSymbolTypeConventions(type, typeSymbols);
|
|
5635
|
+
if (finding) findings.push(finding);
|
|
5379
5636
|
}
|
|
5380
5637
|
const fileNaming = analyzeFileNaming(ctx.files);
|
|
5381
5638
|
if (fileNaming) findings.push(fileNaming);
|
|
@@ -5590,33 +5847,48 @@ function isInterfaceImplPair(a, b) {
|
|
|
5590
5847
|
const bIsInterface = /(?:interface|store|contract|abstract|base|types)\./i.test(b.file) || b.bodyTokenCount < 10;
|
|
5591
5848
|
return aIsInterface && !bIsInterface || !aIsInterface && bIsInterface;
|
|
5592
5849
|
}
|
|
5593
|
-
function
|
|
5850
|
+
function isConventionalName(name) {
|
|
5851
|
+
return CONVENTION_NAMES.has(name.toLowerCase());
|
|
5852
|
+
}
|
|
5853
|
+
function areStructurallySimilar(a, b) {
|
|
5594
5854
|
if (a.domainCategory !== b.domainCategory || a.domainCategory === "general") return false;
|
|
5595
5855
|
if (a.file === b.file) return false;
|
|
5596
5856
|
if (Math.abs(a.paramCount - b.paramCount) > 1) return false;
|
|
5597
5857
|
const sizeRatio = Math.min(a.bodyTokenCount, b.bodyTokenCount) / Math.max(a.bodyTokenCount, b.bodyTokenCount);
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
if (
|
|
5602
|
-
if (nameA
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5858
|
+
return sizeRatio >= 0.4;
|
|
5859
|
+
}
|
|
5860
|
+
function areNamesSimilar(nameA, nameB, hashA, hashB, tokenCountA) {
|
|
5861
|
+
if (nameA === nameB) return true;
|
|
5862
|
+
if (nameA.length >= 6 && nameB.length >= 6 && levenshtein(nameA, nameB) <= 3) return true;
|
|
5863
|
+
if (hashA === hashB && tokenCountA > 20) return true;
|
|
5864
|
+
return false;
|
|
5865
|
+
}
|
|
5866
|
+
function isLayerPair(a, b) {
|
|
5606
5867
|
const aIsRepo = /(?:repository|repo|store|postgres|mysql|mongo|dal)/i.test(a.file);
|
|
5607
5868
|
const bIsRepo = /(?:repository|repo|store|postgres|mysql|mongo|dal)/i.test(b.file);
|
|
5608
5869
|
const aIsHandler = /(?:handler|controller|route|endpoint|api)/i.test(a.file);
|
|
5609
5870
|
const bIsHandler = /(?:handler|controller|route|endpoint|api)/i.test(b.file);
|
|
5610
|
-
if (aIsHandler && bIsRepo || aIsRepo && bIsHandler) return
|
|
5871
|
+
if (aIsHandler && bIsRepo || aIsRepo && bIsHandler) return true;
|
|
5872
|
+
const nameA = a.name.toLowerCase();
|
|
5873
|
+
const nameB = b.name.toLowerCase();
|
|
5611
5874
|
if (nameA === nameB && a.paramCount === b.paramCount) {
|
|
5612
|
-
if (aIsHandler && bIsHandler) return
|
|
5613
|
-
if (aIsRepo && bIsRepo) return
|
|
5875
|
+
if (aIsHandler && bIsHandler) return true;
|
|
5876
|
+
if (aIsRepo && bIsRepo) return true;
|
|
5614
5877
|
}
|
|
5615
|
-
if (nameA === nameB) return true;
|
|
5616
|
-
if (nameA.length >= 6 && nameB.length >= 6 && levenshtein(nameA, nameB) <= 3) return true;
|
|
5617
|
-
if (a.bodyHash === b.bodyHash && a.bodyTokenCount > 20) return true;
|
|
5618
5878
|
return false;
|
|
5619
5879
|
}
|
|
5880
|
+
function areSemanticallySimlar(a, b) {
|
|
5881
|
+
if (!areStructurallySimilar(a, b)) return false;
|
|
5882
|
+
const nameA = a.name.toLowerCase();
|
|
5883
|
+
const nameB = b.name.toLowerCase();
|
|
5884
|
+
if (isConventionalName(nameA) || isConventionalName(nameB)) return false;
|
|
5885
|
+
if (nameA === nameB && /(?:handler|controller|resource|api)/i.test(a.file) && /(?:handler|controller|resource|api)/i.test(b.file)) {
|
|
5886
|
+
if (["list", "get", "create", "update", "delete", "find", "search"].includes(nameA)) return false;
|
|
5887
|
+
}
|
|
5888
|
+
if (nameA === nameB && isInterfaceImplPair(a, b)) return false;
|
|
5889
|
+
if (isLayerPair(a, b)) return false;
|
|
5890
|
+
return areNamesSimilar(nameA, nameB, a.bodyHash, b.bodyHash, a.bodyTokenCount);
|
|
5891
|
+
}
|
|
5620
5892
|
function levenshtein(a, b) {
|
|
5621
5893
|
const m = a.length, n = b.length;
|
|
5622
5894
|
if (m === 0) return n;
|
|
@@ -5848,131 +6120,155 @@ function buildUsageGraph(files) {
|
|
|
5848
6120
|
}
|
|
5849
6121
|
return usage;
|
|
5850
6122
|
}
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
if (
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
if (exp.crudType === "other") continue;
|
|
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
|
-
}
|
|
6123
|
+
function isReferencedExternally(name, usage, currentFile, allRoutes) {
|
|
6124
|
+
const references = usage.get(name);
|
|
6125
|
+
const usedExternally = references && [...references].some((f) => f !== currentFile);
|
|
6126
|
+
const isRouted = allRoutes.some(
|
|
6127
|
+
(r) => r.handlerName.includes(name) || r.handlerName.endsWith("." + name)
|
|
6128
|
+
);
|
|
6129
|
+
return !!(usedExternally || isRouted);
|
|
6130
|
+
}
|
|
6131
|
+
function detectScaffoldingPattern(filePath, crudFunctions, usage, allRoutes) {
|
|
6132
|
+
const usedFunctions = [];
|
|
6133
|
+
const unusedFunctions = [];
|
|
6134
|
+
for (const fn of crudFunctions) {
|
|
6135
|
+
if (isReferencedExternally(fn.name, usage, filePath, allRoutes)) {
|
|
6136
|
+
usedFunctions.push(fn);
|
|
6137
|
+
} else {
|
|
6138
|
+
unusedFunctions.push(fn);
|
|
5923
6139
|
}
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
}
|
|
6140
|
+
}
|
|
6141
|
+
if (unusedFunctions.length < 2 || usedFunctions.length < 1) return null;
|
|
6142
|
+
const usedTypes = usedFunctions.map((f) => f.crudType);
|
|
6143
|
+
const unusedTypes = unusedFunctions.map((f) => f.crudType);
|
|
6144
|
+
const isScaffoldingLike = usedTypes.some((t) => t === "read" || t === "list") && unusedTypes.some((t) => t === "create" || t === "update" || t === "delete");
|
|
6145
|
+
return {
|
|
6146
|
+
detector: "phantom_scaffolding",
|
|
6147
|
+
driftCategory: "phantom_scaffolding",
|
|
6148
|
+
severity: isScaffoldingLike ? "warning" : "info",
|
|
6149
|
+
confidence: isScaffoldingLike ? 0.8 : 0.6,
|
|
6150
|
+
finding: `CRUD scaffolding detected in ${filePath} \u2014 ${usedFunctions.length} functions used, ${unusedFunctions.length} appear unused`,
|
|
6151
|
+
dominantPattern: "used CRUD operations",
|
|
6152
|
+
dominantCount: usedFunctions.length,
|
|
6153
|
+
totalRelevantFiles: usedFunctions.length + unusedFunctions.length,
|
|
6154
|
+
consistencyScore: Math.round(usedFunctions.length / (usedFunctions.length + unusedFunctions.length) * 100),
|
|
6155
|
+
deviatingFiles: [{
|
|
6156
|
+
path: filePath,
|
|
6157
|
+
detectedPattern: `unused: ${unusedFunctions.map((f) => f.name).join(", ")}`,
|
|
6158
|
+
evidence: unusedFunctions.slice(0, 5).map((f) => ({
|
|
6159
|
+
line: f.line,
|
|
6160
|
+
code: `${f.name} (${f.crudType}) \u2014 defined but not routed or imported`
|
|
6161
|
+
}))
|
|
6162
|
+
}],
|
|
6163
|
+
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.`
|
|
6164
|
+
};
|
|
6165
|
+
}
|
|
6166
|
+
function detectUnroutedHandlers(allExports, allRoutes, usage) {
|
|
6167
|
+
const findings = [];
|
|
6168
|
+
const handlerFiles = /* @__PURE__ */ new Map();
|
|
6169
|
+
for (const exp of allExports) {
|
|
6170
|
+
if (exp.crudType === "other") continue;
|
|
6171
|
+
if (!handlerFiles.has(exp.file)) handlerFiles.set(exp.file, []);
|
|
6172
|
+
handlerFiles.get(exp.file).push(exp);
|
|
6173
|
+
}
|
|
6174
|
+
for (const [filePath, functions] of handlerFiles) {
|
|
6175
|
+
const crudFunctions = functions.filter((f) => f.crudType !== "other");
|
|
6176
|
+
if (crudFunctions.length < 2) continue;
|
|
6177
|
+
const finding = detectScaffoldingPattern(filePath, crudFunctions, usage, allRoutes);
|
|
6178
|
+
if (finding) findings.push(finding);
|
|
6179
|
+
}
|
|
6180
|
+
return findings;
|
|
6181
|
+
}
|
|
6182
|
+
function extractTypeDefinitions(files) {
|
|
6183
|
+
const typeDefinitions = [];
|
|
6184
|
+
const goMethodReceivers = /* @__PURE__ */ new Set();
|
|
6185
|
+
for (const file of files) {
|
|
6186
|
+
if (file.language !== "go") continue;
|
|
6187
|
+
const receiverPattern = /func\s+\(\s*\w+\s+\*?(\w+)\s*\)/g;
|
|
6188
|
+
let m;
|
|
6189
|
+
while ((m = receiverPattern.exec(file.content)) !== null) {
|
|
6190
|
+
goMethodReceivers.add(m[1]);
|
|
5942
6191
|
}
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
6192
|
+
}
|
|
6193
|
+
for (const file of files) {
|
|
6194
|
+
if (!file.language) continue;
|
|
6195
|
+
const lines = file.content.split("\n");
|
|
6196
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6197
|
+
const line = lines[i];
|
|
6198
|
+
let typeName = null;
|
|
6199
|
+
if (file.language === "go") {
|
|
6200
|
+
const m = line.match(/^type\s+([A-Z]\w+)\s+struct\b/);
|
|
6201
|
+
if (m) typeName = m[1];
|
|
6202
|
+
} else if (file.language === "javascript" || file.language === "typescript") {
|
|
6203
|
+
const m = line.match(/export\s+(?:interface|type|class)\s+(\w+)/);
|
|
6204
|
+
if (m) typeName = m[1];
|
|
5950
6205
|
}
|
|
6206
|
+
if (!typeName) continue;
|
|
6207
|
+
if (file.language === "go" && goMethodReceivers.has(typeName)) continue;
|
|
6208
|
+
if (/(?:Request|Response|Params|Config|Options|Input|Output|Payload|Body|DTO)$/i.test(typeName)) continue;
|
|
6209
|
+
typeDefinitions.push({ name: typeName, file: file.path, line: i + 1 });
|
|
5951
6210
|
}
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
6211
|
+
}
|
|
6212
|
+
return typeDefinitions;
|
|
6213
|
+
}
|
|
6214
|
+
function findUnusedTypes(typeDefinitions, usage) {
|
|
6215
|
+
const unusedTypesByFile = /* @__PURE__ */ new Map();
|
|
6216
|
+
for (const td of typeDefinitions) {
|
|
6217
|
+
const refs = usage.get(td.name);
|
|
6218
|
+
const usedExternally = refs && [...refs].some((f) => f !== td.file);
|
|
6219
|
+
if (!usedExternally) {
|
|
6220
|
+
if (!unusedTypesByFile.has(td.file)) unusedTypesByFile.set(td.file, []);
|
|
6221
|
+
unusedTypesByFile.get(td.file).push(td);
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
return unusedTypesByFile;
|
|
6225
|
+
}
|
|
6226
|
+
function detectUnusedTypeScaffolding(files, usage) {
|
|
6227
|
+
const findings = [];
|
|
6228
|
+
const typeDefinitions = extractTypeDefinitions(files);
|
|
6229
|
+
const unusedTypesByFile = findUnusedTypes(typeDefinitions, usage);
|
|
6230
|
+
for (const [filePath, unusedTypes] of unusedTypesByFile) {
|
|
6231
|
+
if (unusedTypes.length < 3) continue;
|
|
6232
|
+
findings.push({
|
|
6233
|
+
detector: "phantom_scaffolding",
|
|
6234
|
+
subCategory: "unused_types",
|
|
6235
|
+
driftCategory: "phantom_scaffolding",
|
|
6236
|
+
severity: "info",
|
|
6237
|
+
confidence: 0.55,
|
|
6238
|
+
finding: `${unusedTypes.length} types/structs in ${filePath} appear unused outside their file`,
|
|
6239
|
+
dominantPattern: "used types",
|
|
6240
|
+
dominantCount: typeDefinitions.length - unusedTypes.length,
|
|
6241
|
+
totalRelevantFiles: typeDefinitions.length,
|
|
6242
|
+
consistencyScore: Math.round((typeDefinitions.length - unusedTypes.length) / typeDefinitions.length * 100),
|
|
6243
|
+
deviatingFiles: [{
|
|
6244
|
+
path: filePath,
|
|
6245
|
+
detectedPattern: `unused types: ${unusedTypes.map((t) => t.name).join(", ")}`,
|
|
6246
|
+
evidence: unusedTypes.slice(0, 5).map((t) => ({
|
|
6247
|
+
line: t.line,
|
|
6248
|
+
code: `type ${t.name} \u2014 defined but never imported`
|
|
6249
|
+
}))
|
|
6250
|
+
}],
|
|
6251
|
+
recommendation: `${unusedTypes.length} types in ${filePath.split("/").pop()} are never used outside their file. They may be AI-generated scaffolding that was never needed.`
|
|
6252
|
+
});
|
|
6253
|
+
}
|
|
6254
|
+
return findings;
|
|
6255
|
+
}
|
|
6256
|
+
var phantomScaffolding = {
|
|
6257
|
+
id: "phantom-scaffolding",
|
|
6258
|
+
name: "Phantom Scaffolding",
|
|
6259
|
+
category: "phantom_scaffolding",
|
|
6260
|
+
detect(ctx) {
|
|
6261
|
+
const allExports = [];
|
|
6262
|
+
const allRoutes = [];
|
|
6263
|
+
for (const file of ctx.files) {
|
|
6264
|
+
allExports.push(...extractExportedFunctions(file));
|
|
6265
|
+
allRoutes.push(...extractRouteRegistrations(file));
|
|
5975
6266
|
}
|
|
6267
|
+
if (allExports.length < 3) return [];
|
|
6268
|
+
const usage = buildUsageGraph(ctx.files);
|
|
6269
|
+
const findings = [];
|
|
6270
|
+
findings.push(...detectUnroutedHandlers(allExports, allRoutes, usage));
|
|
6271
|
+
findings.push(...detectUnusedTypeScaffolding(ctx.files, usage));
|
|
5976
6272
|
return findings;
|
|
5977
6273
|
}
|
|
5978
6274
|
};
|
|
@@ -6373,25 +6669,8 @@ function scoreColorFn(score, max) {
|
|
|
6373
6669
|
if (ratio >= 0.5) return chalk.yellow;
|
|
6374
6670
|
return chalk.red;
|
|
6375
6671
|
}
|
|
6376
|
-
function
|
|
6672
|
+
function renderFindingsList(result) {
|
|
6377
6673
|
const lines = [];
|
|
6378
|
-
const banner = ` VibeDrift v${getVersion()} `;
|
|
6379
|
-
const border = "\u2500".repeat(banner.length);
|
|
6380
|
-
lines.push(chalk.cyan(`\u256D${border}\u256E`));
|
|
6381
|
-
lines.push(chalk.cyan(`\u2502${banner}\u2502`));
|
|
6382
|
-
lines.push(chalk.cyan(`\u2570${border}\u256F`));
|
|
6383
|
-
lines.push("");
|
|
6384
|
-
const langCounts = /* @__PURE__ */ new Map();
|
|
6385
|
-
for (const f of result.context.files) {
|
|
6386
|
-
if (f.language) {
|
|
6387
|
-
const label = getLanguageDisplayName(f.language);
|
|
6388
|
-
langCounts.set(label, (langCounts.get(label) ?? 0) + 1);
|
|
6389
|
-
}
|
|
6390
|
-
}
|
|
6391
|
-
const langStr = [...langCounts.entries()].map(([l, c]) => `${l}: ${c}`).join(", ");
|
|
6392
|
-
lines.push(chalk.dim(`Scanning: ${result.context.rootDir}`));
|
|
6393
|
-
lines.push(chalk.dim(`Files: ${result.context.files.length} (${langStr}) | Lines: ${result.context.totalLines.toLocaleString()} | Time: ${(result.scanTimeMs / 1e3).toFixed(1)}s`));
|
|
6394
|
-
lines.push("");
|
|
6395
6674
|
for (const cat of ALL_CATEGORIES) {
|
|
6396
6675
|
const config = CATEGORY_CONFIG[cat];
|
|
6397
6676
|
const s = result.scores[cat];
|
|
@@ -6425,6 +6704,10 @@ function renderTerminalOutput(result) {
|
|
|
6425
6704
|
}
|
|
6426
6705
|
lines.push("");
|
|
6427
6706
|
}
|
|
6707
|
+
return lines;
|
|
6708
|
+
}
|
|
6709
|
+
function renderCategoryBars(result) {
|
|
6710
|
+
const lines = [];
|
|
6428
6711
|
lines.push(chalk.bold("\u2500\u2500 Vibe Debt Score \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6429
6712
|
for (const cat of ALL_CATEGORIES) {
|
|
6430
6713
|
const config = CATEGORY_CONFIG[cat];
|
|
@@ -6448,6 +6731,35 @@ function renderTerminalOutput(result) {
|
|
|
6448
6731
|
const totalColor = scoreColorFn(result.compositeScore, result.maxCompositeScore);
|
|
6449
6732
|
lines.push(` ${padRight("Total Score:", 28)} ${totalColor(`${result.compositeScore.toFixed(0)}/${result.maxCompositeScore}`)}`);
|
|
6450
6733
|
lines.push("");
|
|
6734
|
+
return lines;
|
|
6735
|
+
}
|
|
6736
|
+
function renderScoreSection(result) {
|
|
6737
|
+
const lines = [];
|
|
6738
|
+
const banner = ` VibeDrift v${getVersion()} `;
|
|
6739
|
+
const border = "\u2500".repeat(banner.length);
|
|
6740
|
+
lines.push(chalk.cyan(`\u256D${border}\u256E`));
|
|
6741
|
+
lines.push(chalk.cyan(`\u2502${banner}\u2502`));
|
|
6742
|
+
lines.push(chalk.cyan(`\u2570${border}\u256F`));
|
|
6743
|
+
lines.push("");
|
|
6744
|
+
const langCounts = /* @__PURE__ */ new Map();
|
|
6745
|
+
for (const f of result.context.files) {
|
|
6746
|
+
if (f.language) {
|
|
6747
|
+
const label = getLanguageDisplayName(f.language);
|
|
6748
|
+
langCounts.set(label, (langCounts.get(label) ?? 0) + 1);
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
const langStr = [...langCounts.entries()].map(([l, c]) => `${l}: ${c}`).join(", ");
|
|
6752
|
+
lines.push(chalk.dim(`Scanning: ${result.context.rootDir}`));
|
|
6753
|
+
lines.push(chalk.dim(`Files: ${result.context.files.length} (${langStr}) | Lines: ${result.context.totalLines.toLocaleString()} | Time: ${(result.scanTimeMs / 1e3).toFixed(1)}s`));
|
|
6754
|
+
lines.push("");
|
|
6755
|
+
return lines;
|
|
6756
|
+
}
|
|
6757
|
+
function renderTerminalOutput(result) {
|
|
6758
|
+
const lines = [
|
|
6759
|
+
...renderScoreSection(result),
|
|
6760
|
+
...renderFindingsList(result),
|
|
6761
|
+
...renderCategoryBars(result)
|
|
6762
|
+
];
|
|
6451
6763
|
if (result.deepInsights.length > 0) {
|
|
6452
6764
|
lines.push(chalk.bold("\u2500\u2500 Deep Analysis (AI-Powered) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6453
6765
|
for (const insight of result.deepInsights.slice(0, 10)) {
|
|
@@ -6581,8 +6893,7 @@ function extractIntentPatterns(result) {
|
|
|
6581
6893
|
}
|
|
6582
6894
|
return patterns;
|
|
6583
6895
|
}
|
|
6584
|
-
function
|
|
6585
|
-
const entries = [...result.perFileScores.entries()];
|
|
6896
|
+
function buildDriftFileMap(result) {
|
|
6586
6897
|
const driftFiles = /* @__PURE__ */ new Map();
|
|
6587
6898
|
for (const d of result.driftFindings ?? []) {
|
|
6588
6899
|
for (const df of d.deviatingFiles) {
|
|
@@ -6591,6 +6902,9 @@ function buildFileCoherence(result) {
|
|
|
6591
6902
|
driftFiles.get(df.path).push({ category: catKey, pattern: df.detectedPattern });
|
|
6592
6903
|
}
|
|
6593
6904
|
}
|
|
6905
|
+
return driftFiles;
|
|
6906
|
+
}
|
|
6907
|
+
function buildCategorySet(result) {
|
|
6594
6908
|
const subCatNames = {
|
|
6595
6909
|
data_access: "Data Access",
|
|
6596
6910
|
error_handling: "Error Handling",
|
|
@@ -6611,9 +6925,12 @@ function buildFileCoherence(result) {
|
|
|
6611
6925
|
catSet.set(d.driftCategory, baseCatNames[d.driftCategory]);
|
|
6612
6926
|
}
|
|
6613
6927
|
}
|
|
6614
|
-
|
|
6615
|
-
|
|
6928
|
+
return catSet;
|
|
6929
|
+
}
|
|
6930
|
+
function buildCodeDnaPatternData(result) {
|
|
6616
6931
|
const filePatternMap = /* @__PURE__ */ new Map();
|
|
6932
|
+
let projectDominantPattern = "";
|
|
6933
|
+
const dna = result.codeDnaResult;
|
|
6617
6934
|
if (dna?.patternDistributions?.length > 0) {
|
|
6618
6935
|
const patternCounts = /* @__PURE__ */ new Map();
|
|
6619
6936
|
for (const pd of dna.patternDistributions) {
|
|
@@ -6629,9 +6946,16 @@ function buildFileCoherence(result) {
|
|
|
6629
6946
|
projectDominantPattern = p;
|
|
6630
6947
|
}
|
|
6631
6948
|
}
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6949
|
+
}
|
|
6950
|
+
return { projectDominantPattern, filePatternMap };
|
|
6951
|
+
}
|
|
6952
|
+
function buildFileCoherence(result) {
|
|
6953
|
+
const entries = [...result.perFileScores.entries()];
|
|
6954
|
+
const driftFiles = buildDriftFileMap(result);
|
|
6955
|
+
const catSet = buildCategorySet(result);
|
|
6956
|
+
const { projectDominantPattern, filePatternMap } = buildCodeDnaPatternData(result);
|
|
6957
|
+
if (projectDominantPattern) {
|
|
6958
|
+
catSet.set("codedna_architecture", "Architecture");
|
|
6635
6959
|
}
|
|
6636
6960
|
const categories = [...catSet.values()];
|
|
6637
6961
|
const catKeys = [...catSet.keys()];
|
|
@@ -6704,9 +7028,7 @@ function buildCoherenceMatrix(files) {
|
|
|
6704
7028
|
const colHeaders = categories.map(
|
|
6705
7029
|
(c) => `<th style="padding:6px 12px;font-size:11px;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:0.5px;text-align:center;font-weight:600">${esc(c)}</th>`
|
|
6706
7030
|
).join("");
|
|
6707
|
-
function
|
|
6708
|
-
const pctColor = f.alignmentPct >= 100 ? "var(--intent-green)" : f.alignmentPct >= 50 ? "var(--drift-amber)" : "var(--drift-red)";
|
|
6709
|
-
const bgTint = f.alignmentPct < 50 ? "var(--tint-red)" : f.alignmentPct < 100 ? "var(--tint-amber)" : "transparent";
|
|
7031
|
+
function buildAlignmentCells(f) {
|
|
6710
7032
|
const deviations = [];
|
|
6711
7033
|
const cells = f.alignments.map((a) => {
|
|
6712
7034
|
if (a.matches) return `<td style="text-align:center;padding:6px 12px;color:var(--intent-green);font-size:14px">●</td>`;
|
|
@@ -6714,7 +7036,16 @@ function buildCoherenceMatrix(files) {
|
|
|
6714
7036
|
deviations.push(`${a.category}: ${a.actual ?? "deviates"}`);
|
|
6715
7037
|
return `<td style="text-align:center;padding:6px 12px;color:${devColor};font-size:13px" data-tooltip="${esc(a.actual ?? "deviates")}">◆</td>`;
|
|
6716
7038
|
}).join("");
|
|
6717
|
-
|
|
7039
|
+
return { cells, deviations };
|
|
7040
|
+
}
|
|
7041
|
+
function buildDeviationSummary(deviations) {
|
|
7042
|
+
return deviations.length > 0 ? `<td style="padding:6px 12px;font-size:11px;color:var(--text-secondary);max-width:250px">${deviations.map((d) => esc(d)).join("; ")}</td>` : `<td style="padding:6px 12px;font-size:11px;color:var(--intent-green)">Fully aligned</td>`;
|
|
7043
|
+
}
|
|
7044
|
+
function fileRow(f, open) {
|
|
7045
|
+
const pctColor = f.alignmentPct >= 100 ? "var(--intent-green)" : f.alignmentPct >= 50 ? "var(--drift-amber)" : "var(--drift-red)";
|
|
7046
|
+
const bgTint = f.alignmentPct < 50 ? "var(--tint-red)" : f.alignmentPct < 100 ? "var(--tint-amber)" : "transparent";
|
|
7047
|
+
const { cells, deviations } = buildAlignmentCells(f);
|
|
7048
|
+
const summary = buildDeviationSummary(deviations);
|
|
6718
7049
|
return `<tr style="background:${bgTint}" id="file-${esc(f.path.replace(/[^a-zA-Z0-9]/g, "-"))}">
|
|
6719
7050
|
<td class="mono" style="padding:6px 12px;font-size:12px;color:var(--text-secondary);white-space:nowrap;max-width:280px;overflow:hidden;text-overflow:ellipsis" data-scroll-to="rank-${esc(f.path.replace(/[^a-zA-Z0-9]/g, "-"))}">${esc(f.path)}</td>
|
|
6720
7051
|
${cells}
|
|
@@ -6977,6 +7308,66 @@ function buildFixFirst(result) {
|
|
|
6977
7308
|
${cards}
|
|
6978
7309
|
</section>`;
|
|
6979
7310
|
}
|
|
7311
|
+
function buildDeviatingBlocks(d) {
|
|
7312
|
+
const isConvention = d.driftCategory === "naming_conventions";
|
|
7313
|
+
const devByPattern = /* @__PURE__ */ new Map();
|
|
7314
|
+
for (const df of d.deviatingFiles) {
|
|
7315
|
+
const key = df.detectedPattern;
|
|
7316
|
+
if (!devByPattern.has(key)) devByPattern.set(key, []);
|
|
7317
|
+
devByPattern.get(key).push(df);
|
|
7318
|
+
}
|
|
7319
|
+
if (isConvention && d.deviatingFiles.length > 4) {
|
|
7320
|
+
return [...devByPattern.entries()].map(([pattern, files]) => {
|
|
7321
|
+
const fileList = files.map((df) => esc(df.path)).join("</div><div style='padding:1px 0'>");
|
|
7322
|
+
const collapseId = `conv-${pattern.replace(/[^a-zA-Z0-9]/g, "-")}-${Math.random().toString(36).slice(2, 6)}`;
|
|
7323
|
+
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7324
|
+
<div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT — ${esc(pattern)} — ${files.length} files</div>
|
|
7325
|
+
<div style="cursor:pointer;font-size:12px;color:var(--text-secondary);margin-top:6px" data-collapse="${collapseId}">▶ Show ${files.length} files</div>
|
|
7326
|
+
<div id="${collapseId}" style="display:none;padding:6px 0" class="mono" style="font-size:12px;color:var(--text-secondary)"><div style="padding:1px 0">${fileList}</div></div>
|
|
7327
|
+
</div>`;
|
|
7328
|
+
}).join("");
|
|
7329
|
+
}
|
|
7330
|
+
return d.deviatingFiles.slice(0, 4).map((df) => {
|
|
7331
|
+
const evidence = df.evidence.slice(0, 3).map(
|
|
7332
|
+
(e) => `<div style="background:var(--bg-code);padding:6px 12px;border-radius:0;margin:4px 0;overflow-x:auto" class="mono"><span style="color:var(--text-tertiary);margin-right:12px;user-select:none">${e.line}</span>${esc(e.code.slice(0, 120))}</div>`
|
|
7333
|
+
).join("");
|
|
7334
|
+
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7335
|
+
<div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT — ${esc(df.detectedPattern)}</div>
|
|
7336
|
+
<div class="mono" style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">${esc(df.path)}</div>
|
|
7337
|
+
${evidence}
|
|
7338
|
+
</div>`;
|
|
7339
|
+
}).join("");
|
|
7340
|
+
}
|
|
7341
|
+
function buildDriftRecommendation(d) {
|
|
7342
|
+
let recText = d.recommendation ?? "";
|
|
7343
|
+
if (recText && d.deviatingFiles.length > 0) {
|
|
7344
|
+
const firstDev = d.deviatingFiles[0];
|
|
7345
|
+
const firstEvidence = firstDev.evidence?.[0];
|
|
7346
|
+
if (recText.includes("Migrate deviating files") || recText.includes("Standardize deviating")) {
|
|
7347
|
+
const fileList = d.deviatingFiles.slice(0, 3).map((f) => f.path.split("/").pop()).join(", ");
|
|
7348
|
+
recText = `In ${fileList}${d.deviatingFiles.length > 3 ? ` (+${d.deviatingFiles.length - 3} more)` : ""}: replace ${firstDev.detectedPattern} with the dominant ${d.dominantPattern} pattern (used by ${d.dominantCount}/${d.totalRelevantFiles} files).`;
|
|
7349
|
+
if (firstEvidence) {
|
|
7350
|
+
recText += ` Example: ${firstDev.path.split("/").pop()}:${firstEvidence.line} currently uses ${firstDev.detectedPattern}.`;
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
}
|
|
7354
|
+
return recText;
|
|
7355
|
+
}
|
|
7356
|
+
function buildDistributionBar(d) {
|
|
7357
|
+
const domPct = d.totalRelevantFiles > 0 ? Math.round(d.dominantCount / d.totalRelevantFiles * 100) : 0;
|
|
7358
|
+
const devPct = 100 - domPct;
|
|
7359
|
+
return `<div style="margin:12px 0">
|
|
7360
|
+
<div style="display:flex;height:6px;border-radius:0;overflow:hidden;gap:2px">
|
|
7361
|
+
<div style="width:${domPct}%;background:var(--intent-green);border-radius:0"></div>
|
|
7362
|
+
<div style="width:${devPct}%;background:var(--drift-orange);border-radius:0"></div>
|
|
7363
|
+
</div>
|
|
7364
|
+
<div style="display:flex;gap:16px;margin-top:4px;font-size:11px;color:var(--text-tertiary)">
|
|
7365
|
+
<span>${esc(d.dominantPattern)} (${d.dominantCount})</span>
|
|
7366
|
+
<span>Deviating (${d.totalRelevantFiles - d.dominantCount})</span>
|
|
7367
|
+
<span style="margin-left:auto">${d.consistencyScore}% consistent</span>
|
|
7368
|
+
</div>
|
|
7369
|
+
</div>`;
|
|
7370
|
+
}
|
|
6980
7371
|
function buildDriftFindings(result) {
|
|
6981
7372
|
const driftCats = ["architectural_consistency", "security_posture", "semantic_duplication", "naming_conventions", "phantom_scaffolding"];
|
|
6982
7373
|
const catLabels = {
|
|
@@ -7000,61 +7391,9 @@ function buildDriftFindings(result) {
|
|
|
7000
7391
|
const domBlock = `<div style="background:var(--tint-green);border-left:3px solid var(--intent-green);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7001
7392
|
<div class="label" style="color:var(--intent-green);margin-bottom:6px">INTENT (DOMINANT) — ${esc(d.dominantPattern)} — ${d.dominantCount} files${closeSplitNote}</div>
|
|
7002
7393
|
</div>`;
|
|
7003
|
-
const
|
|
7004
|
-
const
|
|
7005
|
-
|
|
7006
|
-
const key = df.detectedPattern;
|
|
7007
|
-
if (!devByPattern.has(key)) devByPattern.set(key, []);
|
|
7008
|
-
devByPattern.get(key).push(df);
|
|
7009
|
-
}
|
|
7010
|
-
let devBlocks;
|
|
7011
|
-
if (isConvention && d.deviatingFiles.length > 4) {
|
|
7012
|
-
devBlocks = [...devByPattern.entries()].map(([pattern, files]) => {
|
|
7013
|
-
const fileList = files.map((df) => esc(df.path)).join("</div><div style='padding:1px 0'>");
|
|
7014
|
-
const collapseId = `conv-${pattern.replace(/[^a-zA-Z0-9]/g, "-")}-${Math.random().toString(36).slice(2, 6)}`;
|
|
7015
|
-
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7016
|
-
<div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT — ${esc(pattern)} — ${files.length} files</div>
|
|
7017
|
-
<div style="cursor:pointer;font-size:12px;color:var(--text-secondary);margin-top:6px" data-collapse="${collapseId}">▶ Show ${files.length} files</div>
|
|
7018
|
-
<div id="${collapseId}" style="display:none;padding:6px 0" class="mono" style="font-size:12px;color:var(--text-secondary)"><div style="padding:1px 0">${fileList}</div></div>
|
|
7019
|
-
</div>`;
|
|
7020
|
-
}).join("");
|
|
7021
|
-
} else {
|
|
7022
|
-
devBlocks = d.deviatingFiles.slice(0, 4).map((df) => {
|
|
7023
|
-
const evidence = df.evidence.slice(0, 3).map(
|
|
7024
|
-
(e) => `<div style="background:var(--bg-code);padding:6px 12px;border-radius:0;margin:4px 0;overflow-x:auto" class="mono"><span style="color:var(--text-tertiary);margin-right:12px;user-select:none">${e.line}</span>${esc(e.code.slice(0, 120))}</div>`
|
|
7025
|
-
).join("");
|
|
7026
|
-
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7027
|
-
<div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT — ${esc(df.detectedPattern)}</div>
|
|
7028
|
-
<div class="mono" style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">${esc(df.path)}</div>
|
|
7029
|
-
${evidence}
|
|
7030
|
-
</div>`;
|
|
7031
|
-
}).join("");
|
|
7032
|
-
}
|
|
7033
|
-
const domPct = d.totalRelevantFiles > 0 ? Math.round(d.dominantCount / d.totalRelevantFiles * 100) : 0;
|
|
7034
|
-
const devPct = 100 - domPct;
|
|
7035
|
-
const distBar = `<div style="margin:12px 0">
|
|
7036
|
-
<div style="display:flex;height:6px;border-radius:0;overflow:hidden;gap:2px">
|
|
7037
|
-
<div style="width:${domPct}%;background:var(--intent-green);border-radius:0"></div>
|
|
7038
|
-
<div style="width:${devPct}%;background:var(--drift-orange);border-radius:0"></div>
|
|
7039
|
-
</div>
|
|
7040
|
-
<div style="display:flex;gap:16px;margin-top:4px;font-size:11px;color:var(--text-tertiary)">
|
|
7041
|
-
<span>${esc(d.dominantPattern)} (${d.dominantCount})</span>
|
|
7042
|
-
<span>Deviating (${d.totalRelevantFiles - d.dominantCount})</span>
|
|
7043
|
-
<span style="margin-left:auto">${d.consistencyScore}% consistent</span>
|
|
7044
|
-
</div>
|
|
7045
|
-
</div>`;
|
|
7046
|
-
let recText = d.recommendation ?? "";
|
|
7047
|
-
if (recText && d.deviatingFiles.length > 0) {
|
|
7048
|
-
const firstDev = d.deviatingFiles[0];
|
|
7049
|
-
const firstEvidence = firstDev.evidence?.[0];
|
|
7050
|
-
if (recText.includes("Migrate deviating files") || recText.includes("Standardize deviating")) {
|
|
7051
|
-
const fileList = d.deviatingFiles.slice(0, 3).map((f) => f.path.split("/").pop()).join(", ");
|
|
7052
|
-
recText = `In ${fileList}${d.deviatingFiles.length > 3 ? ` (+${d.deviatingFiles.length - 3} more)` : ""}: replace ${firstDev.detectedPattern} with the dominant ${d.dominantPattern} pattern (used by ${d.dominantCount}/${d.totalRelevantFiles} files).`;
|
|
7053
|
-
if (firstEvidence) {
|
|
7054
|
-
recText += ` Example: ${firstDev.path.split("/").pop()}:${firstEvidence.line} currently uses ${firstDev.detectedPattern}.`;
|
|
7055
|
-
}
|
|
7056
|
-
}
|
|
7057
|
-
}
|
|
7394
|
+
const devBlocks = buildDeviatingBlocks(d);
|
|
7395
|
+
const distBar = buildDistributionBar(d);
|
|
7396
|
+
const recText = buildDriftRecommendation(d);
|
|
7058
7397
|
const closeSplitQualifier = domPctVal < 70 && domPctVal >= 50 ? `<div style="margin-top:8px;padding:8px 14px;background:var(--tint-amber);border-left:3px solid var(--drift-amber);border-radius:0;font-size:13px;line-height:1.6;color:var(--text-secondary)">
|
|
7059
7398
|
<strong>${esc(d.dominantPattern)}</strong> is dominant at ${domPctVal}%, but the split is close.
|
|
7060
7399
|
If your team prefers <strong>${esc(d.deviatingFiles[0]?.detectedPattern ?? "the alternative")}</strong>, adopt that instead.
|
|
@@ -7282,6 +7621,37 @@ function buildDeepInsights(result) {
|
|
|
7282
7621
|
${cards}
|
|
7283
7622
|
</section>`;
|
|
7284
7623
|
}
|
|
7624
|
+
function buildSequenceCards(similarities) {
|
|
7625
|
+
const seqCards = similarities.slice(0, 6).map((sim) => {
|
|
7626
|
+
const pct = Math.round(sim.similarity * 100);
|
|
7627
|
+
const color = pct >= 90 ? "var(--drift-red)" : "var(--drift-orange)";
|
|
7628
|
+
const matchLabel = pct >= 100 ? "Exact duplicate" : pct >= 90 ? "Near-exact match" : "Similar";
|
|
7629
|
+
return `<div style="display:flex;align-items:center;gap:12px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
|
|
7630
|
+
<span class="mono" style="min-width:40px;font-weight:700;color:${color}">${pct}%</span>
|
|
7631
|
+
<div style="flex:1">
|
|
7632
|
+
<span style="font-size:11px;font-weight:600;color:${color};margin-right:6px">${matchLabel}</span>
|
|
7633
|
+
<span class="mono" style="font-size:12px;color:var(--text-secondary)">${esc(sim.functionA.name)}()</span>
|
|
7634
|
+
<span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionA.relativePath || sim.functionA.file))}</span>
|
|
7635
|
+
<span style="color:var(--text-tertiary);margin:0 4px">↔</span>
|
|
7636
|
+
<span class="mono" style="font-size:12px;color:var(--drift-orange)">${esc(sim.functionB.name)}()</span>
|
|
7637
|
+
<span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionB.relativePath || sim.functionB.file))}</span>
|
|
7638
|
+
</div>
|
|
7639
|
+
</div>`;
|
|
7640
|
+
}).join("");
|
|
7641
|
+
return `<details style="margin-bottom:8px"><summary style="cursor:pointer;font-size:13px;font-weight:500;color:var(--text-primary);list-style:none;padding:6px 0"><span class="chevron">▶</span> Operation sequence matches <span style="color:var(--text-tertiary);font-weight:400">${similarities.length} pairs</span></summary><div style="padding:4px 0 4px 16px">${seqCards}</div></details>`;
|
|
7642
|
+
}
|
|
7643
|
+
function buildDeviationCards(justifications) {
|
|
7644
|
+
const devCards = justifications.map((dj) => {
|
|
7645
|
+
const vColor = dj.verdict === "likely_justified" ? "var(--intent-green)" : dj.verdict === "likely_accidental" ? "var(--drift-red)" : "var(--drift-amber)";
|
|
7646
|
+
const vLabel = dj.verdict === "likely_justified" ? "JUSTIFIED" : dj.verdict === "likely_accidental" ? "ACCIDENTAL" : "UNCERTAIN";
|
|
7647
|
+
return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
|
|
7648
|
+
<span class="sev-badge" style="background:${vColor};min-width:80px;text-align:center">${vLabel}</span>
|
|
7649
|
+
<span class="mono" style="font-size:12px;color:var(--text-secondary);flex:1">${esc(shortPath(dj.relativePath || dj.file))}</span>
|
|
7650
|
+
<span style="font-size:12px;color:var(--text-tertiary)">${esc(dj.deviatingPattern)} vs ${esc(dj.dominantPattern)}</span>
|
|
7651
|
+
</div>`;
|
|
7652
|
+
}).join("");
|
|
7653
|
+
return `<details style="margin-bottom:8px"><summary style="cursor:pointer;font-size:13px;font-weight:500;color:var(--text-primary);list-style:none;padding:6px 0"><span class="chevron">▶</span> Deviation analysis <span style="color:var(--text-tertiary);font-weight:400">${justifications.length}</span></summary><div style="padding:4px 0 4px 16px">${devCards}</div></details>`;
|
|
7654
|
+
}
|
|
7285
7655
|
function buildCodeDnaSummary(result) {
|
|
7286
7656
|
const dna = result.codeDnaResult;
|
|
7287
7657
|
if (!dna) return "";
|
|
@@ -7292,35 +7662,10 @@ function buildCodeDnaSummary(result) {
|
|
|
7292
7662
|
if (!hasDupes && !hasSeqs && !hasTaint && !hasDevs) return "";
|
|
7293
7663
|
const parts = [];
|
|
7294
7664
|
if (hasSeqs) {
|
|
7295
|
-
|
|
7296
|
-
const pct = Math.round(sim.similarity * 100);
|
|
7297
|
-
const color = pct >= 90 ? "var(--drift-red)" : "var(--drift-orange)";
|
|
7298
|
-
const matchLabel = pct >= 100 ? "Exact duplicate" : pct >= 90 ? "Near-exact match" : "Similar";
|
|
7299
|
-
return `<div style="display:flex;align-items:center;gap:12px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
|
|
7300
|
-
<span class="mono" style="min-width:40px;font-weight:700;color:${color}">${pct}%</span>
|
|
7301
|
-
<div style="flex:1">
|
|
7302
|
-
<span style="font-size:11px;font-weight:600;color:${color};margin-right:6px">${matchLabel}</span>
|
|
7303
|
-
<span class="mono" style="font-size:12px;color:var(--text-secondary)">${esc(sim.functionA.name)}()</span>
|
|
7304
|
-
<span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionA.relativePath || sim.functionA.file))}</span>
|
|
7305
|
-
<span style="color:var(--text-tertiary);margin:0 4px">↔</span>
|
|
7306
|
-
<span class="mono" style="font-size:12px;color:var(--drift-orange)">${esc(sim.functionB.name)}()</span>
|
|
7307
|
-
<span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionB.relativePath || sim.functionB.file))}</span>
|
|
7308
|
-
</div>
|
|
7309
|
-
</div>`;
|
|
7310
|
-
}).join("");
|
|
7311
|
-
parts.push(`<details style="margin-bottom:8px"><summary style="cursor:pointer;font-size:13px;font-weight:500;color:var(--text-primary);list-style:none;padding:6px 0"><span class="chevron">▶</span> Operation sequence matches <span style="color:var(--text-tertiary);font-weight:400">${dna.sequenceSimilarities.length} pairs</span></summary><div style="padding:4px 0 4px 16px">${seqCards}</div></details>`);
|
|
7665
|
+
parts.push(buildSequenceCards(dna.sequenceSimilarities));
|
|
7312
7666
|
}
|
|
7313
7667
|
if (hasDevs) {
|
|
7314
|
-
|
|
7315
|
-
const vColor = dj.verdict === "likely_justified" ? "var(--intent-green)" : dj.verdict === "likely_accidental" ? "var(--drift-red)" : "var(--drift-amber)";
|
|
7316
|
-
const vLabel = dj.verdict === "likely_justified" ? "JUSTIFIED" : dj.verdict === "likely_accidental" ? "ACCIDENTAL" : "UNCERTAIN";
|
|
7317
|
-
return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
|
|
7318
|
-
<span class="sev-badge" style="background:${vColor};min-width:80px;text-align:center">${vLabel}</span>
|
|
7319
|
-
<span class="mono" style="font-size:12px;color:var(--text-secondary);flex:1">${esc(shortPath(dj.relativePath || dj.file))}</span>
|
|
7320
|
-
<span style="font-size:12px;color:var(--text-tertiary)">${esc(dj.deviatingPattern)} vs ${esc(dj.dominantPattern)}</span>
|
|
7321
|
-
</div>`;
|
|
7322
|
-
}).join("");
|
|
7323
|
-
parts.push(`<details style="margin-bottom:8px"><summary style="cursor:pointer;font-size:13px;font-weight:500;color:var(--text-primary);list-style:none;padding:6px 0"><span class="chevron">▶</span> Deviation analysis <span style="color:var(--text-tertiary);font-weight:400">${dna.deviationJustifications.length}</span></summary><div style="padding:4px 0 4px 16px">${devCards}</div></details>`);
|
|
7668
|
+
parts.push(buildDeviationCards(dna.deviationJustifications));
|
|
7324
7669
|
}
|
|
7325
7670
|
if (parts.length === 0) return "";
|
|
7326
7671
|
const totalFindings = dna.findings?.length ?? 0;
|
|
@@ -7715,34 +8060,8 @@ function exportCSV() {
|
|
|
7715
8060
|
}
|
|
7716
8061
|
|
|
7717
8062
|
// \u2500\u2500\u2500\u2500 Export: DOCX (Word-compatible HTML) \u2500\u2500\u2500\u2500
|
|
7718
|
-
function
|
|
7719
|
-
|
|
7720
|
-
if (!d) { alert('Report data not available'); return; }
|
|
7721
|
-
|
|
7722
|
-
let html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word"><head><meta charset="utf-8"><style>';
|
|
7723
|
-
html += 'body{font-family:Calibri,sans-serif;font-size:11pt;color:#222;margin:40px}';
|
|
7724
|
-
html += 'h1{font-size:20pt;color:#0B0F15;border-bottom:2px solid #00D4FF;padding-bottom:8px;margin-top:28px}';
|
|
7725
|
-
html += 'h2{font-size:14pt;color:#333;margin-top:20px}';
|
|
7726
|
-
html += 'h3{font-size:12pt;color:#555;margin-top:14px}';
|
|
7727
|
-
html += 'table{border-collapse:collapse;width:100%;margin:10px 0}';
|
|
7728
|
-
html += 'th,td{border:1px solid #ddd;padding:6px 10px;text-align:left;font-size:10pt}';
|
|
7729
|
-
html += 'th{background:#f0f4f8;font-weight:600}';
|
|
7730
|
-
html += '.sev-critical{color:#dc2626;font-weight:700} .sev-warning{color:#d97706;font-weight:700} .sev-info{color:#2563eb}';
|
|
7731
|
-
html += '.intent{background:#ecfdf5;border-left:3px solid #10b981;padding:8px 12px;margin:6px 0}';
|
|
7732
|
-
html += '.drift{background:#fff7ed;border-left:3px solid #f97316;padding:8px 12px;margin:6px 0}';
|
|
7733
|
-
html += '.rec{background:#f0f9ff;border-left:3px solid #0ea5e9;padding:8px 12px;margin:6px 0}';
|
|
7734
|
-
html += 'code{font-family:Consolas,monospace;font-size:9pt;background:#f5f5f5;padding:1px 4px}';
|
|
7735
|
-
html += '.page-break{page-break-before:always}';
|
|
7736
|
-
html += '</style></head><body>';
|
|
7737
|
-
|
|
7738
|
-
// Title
|
|
7739
|
-
html += '<div style="text-align:center;margin-bottom:30px">';
|
|
7740
|
-
html += '<h1 style="border:none;font-size:28pt;color:#00D4FF">VIBEDRIFT REPORT</h1>';
|
|
7741
|
-
html += '<p style="font-size:16pt;color:#333">' + esc2(d.project) + '</p>';
|
|
7742
|
-
html += '<p>' + d.fileCount + ' files · ' + d.totalLines.toLocaleString() + ' LOC · Score: <strong>' + d.score + '/' + d.maxScore + '</strong></p>';
|
|
7743
|
-
html += '</div>';
|
|
7744
|
-
|
|
7745
|
-
// Drift findings
|
|
8063
|
+
function buildDocxFindingsHtml(d) {
|
|
8064
|
+
let html = '';
|
|
7746
8065
|
html += '<h1>Drift Findings</h1>';
|
|
7747
8066
|
for (const f of d.driftFindings || []) {
|
|
7748
8067
|
const sev = f.severity === 'error' ? 'critical' : f.severity;
|
|
@@ -7752,8 +8071,6 @@ function exportDOCX() {
|
|
|
7752
8071
|
html += '<div class="drift"><strong>DEVIATING:</strong> ' + esc2(f.devFiles) + '</div>';
|
|
7753
8072
|
if (f.recommendation) html += '<div class="rec"><strong>Fix:</strong> ' + esc2(f.recommendation) + '</div>';
|
|
7754
8073
|
}
|
|
7755
|
-
|
|
7756
|
-
// All findings
|
|
7757
8074
|
html += '<div class="page-break"></div><h1>All Findings</h1>';
|
|
7758
8075
|
html += '<table><tr><th>Severity</th><th>Analyzer</th><th>Finding</th><th>File</th><th>Line</th></tr>';
|
|
7759
8076
|
for (const f of d.findings || []) {
|
|
@@ -7762,16 +8079,17 @@ function exportDOCX() {
|
|
|
7762
8079
|
html += '<td><code>' + esc2(f.file) + '</code></td><td>' + f.line + '</td></tr>';
|
|
7763
8080
|
}
|
|
7764
8081
|
html += '</table>';
|
|
8082
|
+
return html;
|
|
8083
|
+
}
|
|
7765
8084
|
|
|
7766
|
-
|
|
8085
|
+
function buildDocxScoresHtml(d) {
|
|
8086
|
+
let html = '';
|
|
7767
8087
|
html += '<div class="page-break"></div><h1>File Scores</h1>';
|
|
7768
8088
|
html += '<table><tr><th>File</th><th>Score</th><th>Findings</th></tr>';
|
|
7769
8089
|
for (const f of d.fileScores || []) {
|
|
7770
8090
|
html += '<tr><td><code>' + esc2(f.file) + '</code></td><td>' + f.score + '/100</td><td>' + f.findings + '</td></tr>';
|
|
7771
8091
|
}
|
|
7772
8092
|
html += '</table>';
|
|
7773
|
-
|
|
7774
|
-
// Code DNA
|
|
7775
8093
|
if (d.codeDna) {
|
|
7776
8094
|
html += '<div class="page-break"></div><h1>Code DNA Analysis</h1>';
|
|
7777
8095
|
if (d.codeDna.sequences && d.codeDna.sequences.length > 0) {
|
|
@@ -7785,8 +8103,6 @@ function exportDOCX() {
|
|
|
7785
8103
|
html += '</table>';
|
|
7786
8104
|
}
|
|
7787
8105
|
}
|
|
7788
|
-
|
|
7789
|
-
// Deep insights
|
|
7790
8106
|
if (d.deepInsights && d.deepInsights.length > 0) {
|
|
7791
8107
|
html += '<div class="page-break"></div><h1>Deep Analysis Insights (AI-Powered)</h1>';
|
|
7792
8108
|
for (const ins of d.deepInsights) {
|
|
@@ -7795,6 +8111,37 @@ function exportDOCX() {
|
|
|
7795
8111
|
if (ins.recommendation) html += '<div class="rec"><strong>Fix:</strong> ' + esc2(ins.recommendation) + '</div>';
|
|
7796
8112
|
}
|
|
7797
8113
|
}
|
|
8114
|
+
return html;
|
|
8115
|
+
}
|
|
8116
|
+
|
|
8117
|
+
function exportDOCX() {
|
|
8118
|
+
const d = window.__VIBEDRIFT_DATA;
|
|
8119
|
+
if (!d) { alert('Report data not available'); return; }
|
|
8120
|
+
|
|
8121
|
+
let html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word"><head><meta charset="utf-8"><style>';
|
|
8122
|
+
html += 'body{font-family:Calibri,sans-serif;font-size:11pt;color:#222;margin:40px}';
|
|
8123
|
+
html += 'h1{font-size:20pt;color:#0B0F15;border-bottom:2px solid #00D4FF;padding-bottom:8px;margin-top:28px}';
|
|
8124
|
+
html += 'h2{font-size:14pt;color:#333;margin-top:20px}';
|
|
8125
|
+
html += 'h3{font-size:12pt;color:#555;margin-top:14px}';
|
|
8126
|
+
html += 'table{border-collapse:collapse;width:100%;margin:10px 0}';
|
|
8127
|
+
html += 'th,td{border:1px solid #ddd;padding:6px 10px;text-align:left;font-size:10pt}';
|
|
8128
|
+
html += 'th{background:#f0f4f8;font-weight:600}';
|
|
8129
|
+
html += '.sev-critical{color:#dc2626;font-weight:700} .sev-warning{color:#d97706;font-weight:700} .sev-info{color:#2563eb}';
|
|
8130
|
+
html += '.intent{background:#ecfdf5;border-left:3px solid #10b981;padding:8px 12px;margin:6px 0}';
|
|
8131
|
+
html += '.drift{background:#fff7ed;border-left:3px solid #f97316;padding:8px 12px;margin:6px 0}';
|
|
8132
|
+
html += '.rec{background:#f0f9ff;border-left:3px solid #0ea5e9;padding:8px 12px;margin:6px 0}';
|
|
8133
|
+
html += 'code{font-family:Consolas,monospace;font-size:9pt;background:#f5f5f5;padding:1px 4px}';
|
|
8134
|
+
html += '.page-break{page-break-before:always}';
|
|
8135
|
+
html += '</style></head><body>';
|
|
8136
|
+
|
|
8137
|
+
html += '<div style="text-align:center;margin-bottom:30px">';
|
|
8138
|
+
html += '<h1 style="border:none;font-size:28pt;color:#00D4FF">VIBEDRIFT REPORT</h1>';
|
|
8139
|
+
html += '<p style="font-size:16pt;color:#333">' + esc2(d.project) + '</p>';
|
|
8140
|
+
html += '<p>' + d.fileCount + ' files · ' + d.totalLines.toLocaleString() + ' LOC · Score: <strong>' + d.score + '/' + d.maxScore + '</strong></p>';
|
|
8141
|
+
html += '</div>';
|
|
8142
|
+
|
|
8143
|
+
html += buildDocxFindingsHtml(d);
|
|
8144
|
+
html += buildDocxScoresHtml(d);
|
|
7798
8145
|
|
|
7799
8146
|
html += '<hr><p style="color:#999;font-size:9pt">Generated by VibeDrift v' + d.version + ' | ' + d.fileCount + ' files | No data sent externally</p>';
|
|
7800
8147
|
html += '</body></html>';
|
|
@@ -8164,18 +8511,7 @@ async function createPortalSession(token, opts) {
|
|
|
8164
8511
|
}
|
|
8165
8512
|
|
|
8166
8513
|
// 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
|
-
}
|
|
8514
|
+
async function resolveAuthAndBanner(options) {
|
|
8179
8515
|
let bearerToken = null;
|
|
8180
8516
|
let apiUrl = options.apiUrl;
|
|
8181
8517
|
if (options.deep) {
|
|
@@ -8222,10 +8558,10 @@ async function runScan(targetPath, options) {
|
|
|
8222
8558
|
console.log("");
|
|
8223
8559
|
}
|
|
8224
8560
|
}
|
|
8225
|
-
|
|
8226
|
-
|
|
8561
|
+
return { bearerToken, apiUrl };
|
|
8562
|
+
}
|
|
8563
|
+
async function discoverAndFilterFiles(rootDir, options, spinner) {
|
|
8227
8564
|
const isTerminal = options.format === "terminal" && !options.json;
|
|
8228
|
-
const spinner = isTerminal ? ora("Discovering files...").start() : null;
|
|
8229
8565
|
const t0 = Date.now();
|
|
8230
8566
|
const { ctx, warnings } = await buildAnalysisContext(rootDir);
|
|
8231
8567
|
const includes = options.include ?? [];
|
|
@@ -8239,7 +8575,7 @@ async function runScan(targetPath, options) {
|
|
|
8239
8575
|
console.error(chalk2.dim(`[filter] ${before} \u2192 ${filtered.length} files after include/exclude`));
|
|
8240
8576
|
}
|
|
8241
8577
|
}
|
|
8242
|
-
|
|
8578
|
+
const discoveryMs = Date.now() - t0;
|
|
8243
8579
|
if (isTerminal) {
|
|
8244
8580
|
if (warnings.truncated) {
|
|
8245
8581
|
console.warn(chalk2.yellow(`
|
|
@@ -8257,6 +8593,13 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8257
8593
|
console.log("No source files found to analyze.");
|
|
8258
8594
|
process.exit(0);
|
|
8259
8595
|
}
|
|
8596
|
+
return { ctx, discoveryMs };
|
|
8597
|
+
}
|
|
8598
|
+
async function runAnalysisPipeline(rootDir, options, spinner) {
|
|
8599
|
+
const isTerminal = options.format === "terminal" && !options.json;
|
|
8600
|
+
const timings = {};
|
|
8601
|
+
const { ctx, discoveryMs } = await discoverAndFilterFiles(rootDir, options, spinner);
|
|
8602
|
+
timings.discovery = discoveryMs;
|
|
8260
8603
|
if (spinner) spinner.text = `Parsing ${ctx.files.length} files...`;
|
|
8261
8604
|
const t1 = Date.now();
|
|
8262
8605
|
await parseFiles(ctx.files);
|
|
@@ -8288,30 +8631,33 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8288
8631
|
console.error(`[codedna] ${codeDnaResult.duplicateGroups.length} fingerprint duplicates, ${codeDnaResult.sequenceSimilarities.length} sequence matches, ${codeDnaResult.taintFlows.length} taint flows`);
|
|
8289
8632
|
}
|
|
8290
8633
|
}
|
|
8634
|
+
return { ctx, allFindings, driftResult, codeDnaResult, timings };
|
|
8635
|
+
}
|
|
8636
|
+
async function runDeepAnalysis(pipeline, options, bearerToken, apiUrl, spinner) {
|
|
8637
|
+
const { allFindings, ctx, codeDnaResult, driftResult } = pipeline;
|
|
8638
|
+
const timings = {};
|
|
8291
8639
|
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."));
|
|
8640
|
+
const t5 = Date.now();
|
|
8641
|
+
if (spinner) spinner.text = "Running AI deep analysis (may take ~30s on cold start)...";
|
|
8642
|
+
try {
|
|
8643
|
+
const { runMlAnalysis: runMlAnalysis2 } = await Promise.resolve().then(() => (init_ml_client(), ml_client_exports));
|
|
8644
|
+
const mlResult = await runMlAnalysis2(ctx, codeDnaResult, allFindings, {
|
|
8645
|
+
token: bearerToken,
|
|
8646
|
+
apiUrl,
|
|
8647
|
+
verbose: options.verbose,
|
|
8648
|
+
driftFindings: driftResult.driftFindings,
|
|
8649
|
+
projectName: options.projectName
|
|
8650
|
+
});
|
|
8651
|
+
allFindings.push(...mlResult.highConfidence);
|
|
8652
|
+
mlMediumConfidence = mlResult.mediumConfidence;
|
|
8653
|
+
if (options.verbose) {
|
|
8654
|
+
console.error(`[deep] ${mlResult.highConfidence.length} high-confidence findings shipped, ${mlResult.mediumConfidence.length} sent to LLM, ${mlResult.droppedCount} dropped`);
|
|
8312
8655
|
}
|
|
8313
|
-
|
|
8656
|
+
} catch (err) {
|
|
8657
|
+
console.error(chalk2.red(`[deep] AI analysis failed: ${err.message}`));
|
|
8658
|
+
console.error(chalk2.dim(" The local scan will continue. Run `vibedrift doctor` if this persists."));
|
|
8314
8659
|
}
|
|
8660
|
+
timings.deep = Date.now() - t5;
|
|
8315
8661
|
const { deduplicateFindingsAcrossLayers: deduplicateFindingsAcrossLayers2 } = await Promise.resolve().then(() => (init_dedup(), dedup_exports));
|
|
8316
8662
|
const dedupedCount = allFindings.length;
|
|
8317
8663
|
const dedupedFindings = deduplicateFindingsAcrossLayers2(allFindings);
|
|
@@ -8320,6 +8666,12 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8320
8666
|
}
|
|
8321
8667
|
allFindings.length = 0;
|
|
8322
8668
|
allFindings.push(...dedupedFindings);
|
|
8669
|
+
void mlMediumConfidence;
|
|
8670
|
+
return { timings };
|
|
8671
|
+
}
|
|
8672
|
+
async function buildScanResult(pipeline, options, startTime, timings, bearerToken, apiUrl, spinner) {
|
|
8673
|
+
const { ctx, allFindings, driftResult, codeDnaResult } = pipeline;
|
|
8674
|
+
const rootDir = ctx.rootDir;
|
|
8323
8675
|
const previousScores = await loadPreviousScores(rootDir);
|
|
8324
8676
|
if (spinner) spinner.text = "Computing scores...";
|
|
8325
8677
|
const { scores, compositeScore, maxCompositeScore, perFileScores } = computeScores(
|
|
@@ -8369,7 +8721,10 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8369
8721
|
if (options.verbose) console.error(`[summary] Failed: ${err.message}`);
|
|
8370
8722
|
}
|
|
8371
8723
|
}
|
|
8372
|
-
|
|
8724
|
+
return result;
|
|
8725
|
+
}
|
|
8726
|
+
async function logAndRender(result, options, bearerToken, apiUrl, rootDir, codeDnaResult) {
|
|
8727
|
+
const { findings: allFindings, compositeScore, maxCompositeScore, scanTimeMs } = result;
|
|
8373
8728
|
if (bearerToken) {
|
|
8374
8729
|
try {
|
|
8375
8730
|
const { logScan: logScan2 } = await Promise.resolve().then(() => (init_log_scan(), log_scan_exports));
|
|
@@ -8405,8 +8760,8 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8405
8760
|
payload: {
|
|
8406
8761
|
project_hash: projectIdentity.hash,
|
|
8407
8762
|
project_name: projectIdentity.name,
|
|
8408
|
-
language:
|
|
8409
|
-
file_count:
|
|
8763
|
+
language: result.context.dominantLanguage ?? "unknown",
|
|
8764
|
+
file_count: result.context.files.length,
|
|
8410
8765
|
function_count: codeDnaResult?.functions?.length ?? 0,
|
|
8411
8766
|
finding_count: allFindings.length,
|
|
8412
8767
|
score: compositeScore,
|
|
@@ -8427,6 +8782,12 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8427
8782
|
}
|
|
8428
8783
|
}
|
|
8429
8784
|
const format = options.format ?? (options.json ? "json" : "html");
|
|
8785
|
+
await renderToFormat(result, format, options);
|
|
8786
|
+
if (options.failOnScore !== void 0 && compositeScore < options.failOnScore) {
|
|
8787
|
+
process.exit(1);
|
|
8788
|
+
}
|
|
8789
|
+
}
|
|
8790
|
+
async function renderToFormat(result, format, options) {
|
|
8430
8791
|
if (format === "html") {
|
|
8431
8792
|
const html = renderHtmlReport(result);
|
|
8432
8793
|
const outputPath = options.output ?? "vibedrift-report.html";
|
|
@@ -8475,9 +8836,29 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8475
8836
|
} else {
|
|
8476
8837
|
console.log(renderTerminalOutput(result));
|
|
8477
8838
|
}
|
|
8478
|
-
|
|
8839
|
+
}
|
|
8840
|
+
async function runScan(targetPath, options) {
|
|
8841
|
+
const rootDir = resolve(targetPath);
|
|
8842
|
+
try {
|
|
8843
|
+
const info2 = await stat3(rootDir);
|
|
8844
|
+
if (!info2.isDirectory()) {
|
|
8845
|
+
console.error(`Error: ${rootDir} is not a directory`);
|
|
8846
|
+
process.exit(1);
|
|
8847
|
+
}
|
|
8848
|
+
} catch {
|
|
8849
|
+
console.error(`Error: ${rootDir} does not exist`);
|
|
8479
8850
|
process.exit(1);
|
|
8480
8851
|
}
|
|
8852
|
+
const { bearerToken, apiUrl } = await resolveAuthAndBanner(options);
|
|
8853
|
+
const startTime = Date.now();
|
|
8854
|
+
const isTerminal = options.format === "terminal" && !options.json;
|
|
8855
|
+
const spinner = isTerminal ? ora("Discovering files...").start() : null;
|
|
8856
|
+
const pipeline = await runAnalysisPipeline(rootDir, options, spinner);
|
|
8857
|
+
const deepTimings = options.deep && bearerToken ? await runDeepAnalysis(pipeline, options, bearerToken, apiUrl, spinner) : { timings: {} };
|
|
8858
|
+
const timings = { ...pipeline.timings, ...deepTimings.timings };
|
|
8859
|
+
const result = await buildScanResult(pipeline, options, startTime, timings, bearerToken, apiUrl, spinner);
|
|
8860
|
+
await saveScanResult(rootDir, result.scores, result.compositeScore);
|
|
8861
|
+
await logAndRender(result, options, bearerToken, apiUrl, rootDir, pipeline.codeDnaResult);
|
|
8481
8862
|
}
|
|
8482
8863
|
|
|
8483
8864
|
// src/cli/commands/update.ts
|
|
@@ -8669,58 +9050,7 @@ async function runLogin(options = {}) {
|
|
|
8669
9050
|
}
|
|
8670
9051
|
if (result.status === "pending") continue;
|
|
8671
9052
|
if (result.status === "authorized") {
|
|
8672
|
-
await
|
|
8673
|
-
token: result.access_token,
|
|
8674
|
-
email: result.email,
|
|
8675
|
-
plan: result.plan,
|
|
8676
|
-
expiresAt: result.expires_at,
|
|
8677
|
-
loggedInAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8678
|
-
apiUrl: options.apiUrl
|
|
8679
|
-
});
|
|
8680
|
-
console.log(chalk4.green(" \u2713 Logged in successfully."));
|
|
8681
|
-
console.log("");
|
|
8682
|
-
console.log(` Account: ${chalk4.bold(result.email)}`);
|
|
8683
|
-
console.log(` Plan: ${chalk4.bold(result.plan)}`);
|
|
8684
|
-
console.log("");
|
|
8685
|
-
try {
|
|
8686
|
-
const credits = await fetchCredits(result.access_token, {
|
|
8687
|
-
apiUrl: options.apiUrl
|
|
8688
|
-
});
|
|
8689
|
-
if (credits.has_free_deep_scan && !credits.unlimited) {
|
|
8690
|
-
console.log(
|
|
8691
|
-
chalk4.bgYellow.black.bold(" \u{1F381} 1 FREE deep scan included with your account ")
|
|
8692
|
-
);
|
|
8693
|
-
console.log("");
|
|
8694
|
-
console.log(
|
|
8695
|
-
chalk4.yellow(" Try the full pipeline (Claude analysis, security review,")
|
|
8696
|
-
);
|
|
8697
|
-
console.log(
|
|
8698
|
-
chalk4.yellow(" AI-powered drift detection) on any project \u2014 no card needed.")
|
|
8699
|
-
);
|
|
8700
|
-
console.log("");
|
|
8701
|
-
console.log(` ${chalk4.cyan("vibedrift . --deep")}`);
|
|
8702
|
-
console.log("");
|
|
8703
|
-
} else if (credits.unlimited) {
|
|
8704
|
-
console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
|
|
8705
|
-
console.log("");
|
|
8706
|
-
} else if (credits.available_total > 0) {
|
|
8707
|
-
console.log(
|
|
8708
|
-
chalk4.dim(` You have ${credits.available_total} deep scan credit${credits.available_total === 1 ? "" : "s"} available.`)
|
|
8709
|
-
);
|
|
8710
|
-
console.log(chalk4.dim(" Run `vibedrift . --deep` to use one."));
|
|
8711
|
-
console.log("");
|
|
8712
|
-
} else {
|
|
8713
|
-
console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
|
|
8714
|
-
console.log("");
|
|
8715
|
-
}
|
|
8716
|
-
} catch {
|
|
8717
|
-
if (result.plan === "free") {
|
|
8718
|
-
console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
|
|
8719
|
-
} else {
|
|
8720
|
-
console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
|
|
8721
|
-
}
|
|
8722
|
-
console.log("");
|
|
8723
|
-
}
|
|
9053
|
+
await handleLoginSuccess(result, options);
|
|
8724
9054
|
return;
|
|
8725
9055
|
}
|
|
8726
9056
|
if (result.status === "denied") {
|
|
@@ -8738,6 +9068,60 @@ async function runLogin(options = {}) {
|
|
|
8738
9068
|
console.error(chalk4.dim(" Run `vibedrift login` again to retry.\n"));
|
|
8739
9069
|
process.exit(1);
|
|
8740
9070
|
}
|
|
9071
|
+
async function handleLoginSuccess(result, options) {
|
|
9072
|
+
await patchConfig({
|
|
9073
|
+
token: result.access_token,
|
|
9074
|
+
email: result.email,
|
|
9075
|
+
plan: result.plan,
|
|
9076
|
+
expiresAt: result.expires_at,
|
|
9077
|
+
loggedInAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9078
|
+
apiUrl: options.apiUrl
|
|
9079
|
+
});
|
|
9080
|
+
console.log(chalk4.green(" \u2713 Logged in successfully."));
|
|
9081
|
+
console.log("");
|
|
9082
|
+
console.log(` Account: ${chalk4.bold(result.email)}`);
|
|
9083
|
+
console.log(` Plan: ${chalk4.bold(result.plan)}`);
|
|
9084
|
+
console.log("");
|
|
9085
|
+
try {
|
|
9086
|
+
const credits = await fetchCredits(result.access_token, {
|
|
9087
|
+
apiUrl: options.apiUrl
|
|
9088
|
+
});
|
|
9089
|
+
if (credits.has_free_deep_scan && !credits.unlimited) {
|
|
9090
|
+
console.log(
|
|
9091
|
+
chalk4.bgYellow.black.bold(" \u{1F381} 1 FREE deep scan included with your account ")
|
|
9092
|
+
);
|
|
9093
|
+
console.log("");
|
|
9094
|
+
console.log(
|
|
9095
|
+
chalk4.yellow(" Try the full pipeline (Claude analysis, security review,")
|
|
9096
|
+
);
|
|
9097
|
+
console.log(
|
|
9098
|
+
chalk4.yellow(" AI-powered drift detection) on any project \u2014 no card needed.")
|
|
9099
|
+
);
|
|
9100
|
+
console.log("");
|
|
9101
|
+
console.log(` ${chalk4.cyan("vibedrift . --deep")}`);
|
|
9102
|
+
console.log("");
|
|
9103
|
+
} else if (credits.unlimited) {
|
|
9104
|
+
console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
|
|
9105
|
+
console.log("");
|
|
9106
|
+
} else if (credits.available_total > 0) {
|
|
9107
|
+
console.log(
|
|
9108
|
+
chalk4.dim(` You have ${credits.available_total} deep scan credit${credits.available_total === 1 ? "" : "s"} available.`)
|
|
9109
|
+
);
|
|
9110
|
+
console.log(chalk4.dim(" Run `vibedrift . --deep` to use one."));
|
|
9111
|
+
console.log("");
|
|
9112
|
+
} else {
|
|
9113
|
+
console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
|
|
9114
|
+
console.log("");
|
|
9115
|
+
}
|
|
9116
|
+
} catch {
|
|
9117
|
+
if (result.plan === "free") {
|
|
9118
|
+
console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
|
|
9119
|
+
} else {
|
|
9120
|
+
console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
|
|
9121
|
+
}
|
|
9122
|
+
console.log("");
|
|
9123
|
+
}
|
|
9124
|
+
}
|
|
8741
9125
|
function fail(intro, err) {
|
|
8742
9126
|
const msg = err instanceof VibeDriftApiError ? `${err.status ? `HTTP ${err.status}: ` : ""}${err.message}` : err instanceof Error ? err.message : String(err);
|
|
8743
9127
|
console.error(chalk4.red(`
|
|
@@ -8985,58 +9369,8 @@ import { homedir as homedir3, platform, arch } from "os";
|
|
|
8985
9369
|
import { join as join7 } from "path";
|
|
8986
9370
|
import { stat as stat4, access, constants } from "fs/promises";
|
|
8987
9371
|
init_version();
|
|
8988
|
-
async function
|
|
8989
|
-
let
|
|
8990
|
-
console.log("");
|
|
8991
|
-
console.log(chalk10.bold(" VibeDrift Doctor"));
|
|
8992
|
-
console.log("");
|
|
8993
|
-
console.log(chalk10.bold(" Environment"));
|
|
8994
|
-
ok("CLI version", getVersion());
|
|
8995
|
-
ok("Node", process.version);
|
|
8996
|
-
ok("Platform", `${platform()} ${arch()}`);
|
|
8997
|
-
ok("HOME", homedir3());
|
|
8998
|
-
console.log("");
|
|
8999
|
-
console.log(chalk10.bold(" Config"));
|
|
9000
|
-
const configDir = getConfigDir();
|
|
9001
|
-
const configPath = getConfigPath();
|
|
9002
|
-
let configDirOk = false;
|
|
9003
|
-
try {
|
|
9004
|
-
const info2 = await stat4(configDir);
|
|
9005
|
-
if (info2.isDirectory()) {
|
|
9006
|
-
configDirOk = true;
|
|
9007
|
-
const mode = (info2.mode & 511).toString(8);
|
|
9008
|
-
ok("Config dir", `${configDir} (mode ${mode})`);
|
|
9009
|
-
} else {
|
|
9010
|
-
bad(`Config dir exists but is not a directory: ${configDir}`);
|
|
9011
|
-
failures++;
|
|
9012
|
-
}
|
|
9013
|
-
} catch {
|
|
9014
|
-
info("Config dir", `${configDir} (will be created on first login)`);
|
|
9015
|
-
configDirOk = true;
|
|
9016
|
-
}
|
|
9017
|
-
if (configDirOk) {
|
|
9018
|
-
try {
|
|
9019
|
-
await access(configPath, constants.R_OK);
|
|
9020
|
-
const info2 = await stat4(configPath);
|
|
9021
|
-
const mode = (info2.mode & 511).toString(8);
|
|
9022
|
-
if ((info2.mode & 63) !== 0) {
|
|
9023
|
-
warn("Config file", `${configPath} (mode ${mode}, world/group readable \u2014 should be 600)`);
|
|
9024
|
-
} else {
|
|
9025
|
-
ok("Config file", `${configPath} (mode ${mode})`);
|
|
9026
|
-
}
|
|
9027
|
-
} catch {
|
|
9028
|
-
info("Config file", "absent (not logged in)");
|
|
9029
|
-
}
|
|
9030
|
-
}
|
|
9031
|
-
const historyDir = join7(homedir3(), ".vibedrift", "scans");
|
|
9032
|
-
try {
|
|
9033
|
-
const info2 = await stat4(historyDir);
|
|
9034
|
-
if (info2.isDirectory()) ok("Scan history", historyDir);
|
|
9035
|
-
else warn("Scan history", `${historyDir} exists but is not a directory`);
|
|
9036
|
-
} catch {
|
|
9037
|
-
info("Scan history", "empty (no scans run yet)");
|
|
9038
|
-
}
|
|
9039
|
-
console.log("");
|
|
9372
|
+
async function checkAuthStatus() {
|
|
9373
|
+
let authFailures = 0;
|
|
9040
9374
|
console.log(chalk10.bold(" Authentication"));
|
|
9041
9375
|
const config = await readConfig();
|
|
9042
9376
|
const resolved = await resolveToken();
|
|
@@ -9053,7 +9387,7 @@ async function runDoctor() {
|
|
|
9053
9387
|
const now = Date.now();
|
|
9054
9388
|
if (expires < now) {
|
|
9055
9389
|
bad(`Token expired ${Math.floor((now - expires) / 864e5)} days ago`);
|
|
9056
|
-
|
|
9390
|
+
authFailures++;
|
|
9057
9391
|
} else {
|
|
9058
9392
|
ok("Token expires", `${config.expiresAt} (${Math.ceil((expires - now) / 864e5)} days)`);
|
|
9059
9393
|
}
|
|
@@ -9061,6 +9395,10 @@ async function runDoctor() {
|
|
|
9061
9395
|
}
|
|
9062
9396
|
}
|
|
9063
9397
|
console.log("");
|
|
9398
|
+
return { resolved, authFailures };
|
|
9399
|
+
}
|
|
9400
|
+
async function checkApiConnectivity(resolved) {
|
|
9401
|
+
let failures = 0;
|
|
9064
9402
|
console.log(chalk10.bold(" API"));
|
|
9065
9403
|
const apiUrl = await resolveApiUrl();
|
|
9066
9404
|
ok("API URL", apiUrl);
|
|
@@ -9095,6 +9433,64 @@ async function runDoctor() {
|
|
|
9095
9433
|
}
|
|
9096
9434
|
}
|
|
9097
9435
|
console.log("");
|
|
9436
|
+
return failures;
|
|
9437
|
+
}
|
|
9438
|
+
async function runDoctor() {
|
|
9439
|
+
let failures = 0;
|
|
9440
|
+
console.log("");
|
|
9441
|
+
console.log(chalk10.bold(" VibeDrift Doctor"));
|
|
9442
|
+
console.log("");
|
|
9443
|
+
console.log(chalk10.bold(" Environment"));
|
|
9444
|
+
ok("CLI version", getVersion());
|
|
9445
|
+
ok("Node", process.version);
|
|
9446
|
+
ok("Platform", `${platform()} ${arch()}`);
|
|
9447
|
+
ok("HOME", homedir3());
|
|
9448
|
+
console.log("");
|
|
9449
|
+
console.log(chalk10.bold(" Config"));
|
|
9450
|
+
const configDir = getConfigDir();
|
|
9451
|
+
const configPath = getConfigPath();
|
|
9452
|
+
let configDirOk = false;
|
|
9453
|
+
try {
|
|
9454
|
+
const info2 = await stat4(configDir);
|
|
9455
|
+
if (info2.isDirectory()) {
|
|
9456
|
+
configDirOk = true;
|
|
9457
|
+
const mode = (info2.mode & 511).toString(8);
|
|
9458
|
+
ok("Config dir", `${configDir} (mode ${mode})`);
|
|
9459
|
+
} else {
|
|
9460
|
+
bad(`Config dir exists but is not a directory: ${configDir}`);
|
|
9461
|
+
failures++;
|
|
9462
|
+
}
|
|
9463
|
+
} catch {
|
|
9464
|
+
info("Config dir", `${configDir} (will be created on first login)`);
|
|
9465
|
+
configDirOk = true;
|
|
9466
|
+
}
|
|
9467
|
+
if (configDirOk) {
|
|
9468
|
+
try {
|
|
9469
|
+
await access(configPath, constants.R_OK);
|
|
9470
|
+
const info2 = await stat4(configPath);
|
|
9471
|
+
const mode = (info2.mode & 511).toString(8);
|
|
9472
|
+
if ((info2.mode & 63) !== 0) {
|
|
9473
|
+
warn("Config file", `${configPath} (mode ${mode}, world/group readable \u2014 should be 600)`);
|
|
9474
|
+
} else {
|
|
9475
|
+
ok("Config file", `${configPath} (mode ${mode})`);
|
|
9476
|
+
}
|
|
9477
|
+
} catch {
|
|
9478
|
+
info("Config file", "absent (not logged in)");
|
|
9479
|
+
}
|
|
9480
|
+
}
|
|
9481
|
+
const historyDir = join7(homedir3(), ".vibedrift", "scans");
|
|
9482
|
+
try {
|
|
9483
|
+
const info2 = await stat4(historyDir);
|
|
9484
|
+
if (info2.isDirectory()) ok("Scan history", historyDir);
|
|
9485
|
+
else warn("Scan history", `${historyDir} exists but is not a directory`);
|
|
9486
|
+
} catch {
|
|
9487
|
+
info("Scan history", "empty (no scans run yet)");
|
|
9488
|
+
}
|
|
9489
|
+
console.log("");
|
|
9490
|
+
const { resolved, authFailures } = await checkAuthStatus();
|
|
9491
|
+
failures += authFailures;
|
|
9492
|
+
const apiFailures = await checkApiConnectivity(resolved);
|
|
9493
|
+
failures += apiFailures;
|
|
9098
9494
|
if (failures === 0) {
|
|
9099
9495
|
console.log(chalk10.green(" \u2713 All checks passed."));
|
|
9100
9496
|
} else {
|