@vibedrift/cli 0.5.0 → 0.5.2
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 +977 -686
- 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
|
-
|
|
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
|
+
));
|
|
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
|
+
));
|
|
1878
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
|
-
for (const dj of dna.deviationJustifications) {
|
|
2165
|
-
const verdict = dj.verdict === "likely_justified" ? "JUSTIFIED" : dj.verdict === "likely_accidental" ? "ACCIDENTAL" : "UNCERTAIN";
|
|
2166
|
-
parts.push(para(` [${verdict}] ${dj.relativePath || dj.file}: uses ${dj.deviatingPattern} vs project ${dj.dominantPattern} (score: ${Math.round(dj.justificationScore * 100)}%)`));
|
|
2167
|
-
}
|
|
2168
|
-
parts.push(para(""));
|
|
2169
|
-
}
|
|
2170
|
-
if (dna.patternDistributions?.length > 0) {
|
|
2171
|
-
parts.push(para("Pattern Classification", "Heading2"));
|
|
2172
|
-
for (const pd of dna.patternDistributions) {
|
|
2173
|
-
const mixed = pd.isInternallyInconsistent ? " [MIXED]" : "";
|
|
2174
|
-
parts.push(para(` ${pd.relativePath || pd.file}: ${pd.dominantPattern} (${Math.round(pd.confidence * 100)}% confidence)${mixed}`));
|
|
2175
|
-
}
|
|
2176
|
-
parts.push(para(""));
|
|
2177
|
-
}
|
|
2178
|
-
parts.push(hr());
|
|
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"}`));
|
|
2179
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}`));
|
|
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
|
|
|
@@ -4167,35 +4318,47 @@ var complexityAnalyzer = {
|
|
|
4167
4318
|
const fns = file.tree ? extractFunctions(file.tree.rootNode, file.relativePath, file.language) : extractFunctionsRegex(file.content, file.relativePath);
|
|
4168
4319
|
allFunctions.push(...fns);
|
|
4169
4320
|
}
|
|
4170
|
-
const
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
})
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
}
|
|
4197
|
-
|
|
4198
|
-
|
|
4321
|
+
for (const fn of allFunctions) {
|
|
4322
|
+
if (fn.complexity > 20) {
|
|
4323
|
+
findings.push({
|
|
4324
|
+
analyzerId: "complexity",
|
|
4325
|
+
severity: "error",
|
|
4326
|
+
confidence: 0.9,
|
|
4327
|
+
message: `Function "${fn.name}" has cyclomatic complexity ${fn.complexity} (threshold: 20)`,
|
|
4328
|
+
locations: [{
|
|
4329
|
+
file: fn.file,
|
|
4330
|
+
line: fn.line,
|
|
4331
|
+
snippet: `${fn.name}() \u2014 ${fn.lineCount} lines, complexity ${fn.complexity}`
|
|
4332
|
+
}],
|
|
4333
|
+
tags: ["complexity", "critical"]
|
|
4334
|
+
});
|
|
4335
|
+
} else if (fn.complexity > 15) {
|
|
4336
|
+
findings.push({
|
|
4337
|
+
analyzerId: "complexity",
|
|
4338
|
+
severity: "warning",
|
|
4339
|
+
confidence: 0.75,
|
|
4340
|
+
message: `Function "${fn.name}" has cyclomatic complexity ${fn.complexity} (threshold: 15)`,
|
|
4341
|
+
locations: [{
|
|
4342
|
+
file: fn.file,
|
|
4343
|
+
line: fn.line,
|
|
4344
|
+
snippet: `${fn.name}() \u2014 ${fn.lineCount} lines, complexity ${fn.complexity}`
|
|
4345
|
+
}],
|
|
4346
|
+
tags: ["complexity", "high"]
|
|
4347
|
+
});
|
|
4348
|
+
} else if (fn.complexity > 10) {
|
|
4349
|
+
findings.push({
|
|
4350
|
+
analyzerId: "complexity",
|
|
4351
|
+
severity: "info",
|
|
4352
|
+
confidence: 0.5,
|
|
4353
|
+
message: `Function "${fn.name}" has cyclomatic complexity ${fn.complexity} (threshold: 10)`,
|
|
4354
|
+
locations: [{
|
|
4355
|
+
file: fn.file,
|
|
4356
|
+
line: fn.line,
|
|
4357
|
+
snippet: `${fn.name}() \u2014 ${fn.lineCount} lines, complexity ${fn.complexity}`
|
|
4358
|
+
}],
|
|
4359
|
+
tags: ["complexity", "moderate"]
|
|
4360
|
+
});
|
|
4361
|
+
}
|
|
4199
4362
|
}
|
|
4200
4363
|
if (allFunctions.length > 0) {
|
|
4201
4364
|
const avgComplexity = allFunctions.reduce((s, f) => s + f.complexity, 0) / allFunctions.length;
|
|
@@ -5153,21 +5316,8 @@ function buildPatternDistribution(profiles) {
|
|
|
5153
5316
|
}
|
|
5154
5317
|
return counts;
|
|
5155
5318
|
}
|
|
5156
|
-
function
|
|
5157
|
-
const
|
|
5158
|
-
if (counts.size < 2) return null;
|
|
5159
|
-
let dominant = null;
|
|
5160
|
-
let dominantCount = 0;
|
|
5161
|
-
for (const [pattern, data] of counts) {
|
|
5162
|
-
if (data.count > dominantCount) {
|
|
5163
|
-
dominantCount = data.count;
|
|
5164
|
-
dominant = pattern;
|
|
5165
|
-
}
|
|
5166
|
-
}
|
|
5167
|
-
if (!dominant) return null;
|
|
5168
|
-
const totalFiles = profiles.length;
|
|
5169
|
-
const consistencyScore = Math.round(dominantCount / totalFiles * 100);
|
|
5170
|
-
const deviating = [];
|
|
5319
|
+
function collectDeviatingFiles(counts, dominant, profiles, patternNames) {
|
|
5320
|
+
const deviating = [];
|
|
5171
5321
|
for (const [pattern, data] of counts) {
|
|
5172
5322
|
if (pattern === dominant) continue;
|
|
5173
5323
|
for (const filePath of data.files) {
|
|
@@ -5180,6 +5330,29 @@ function analyzePatternDistribution(profiles, patternNames) {
|
|
|
5180
5330
|
});
|
|
5181
5331
|
}
|
|
5182
5332
|
}
|
|
5333
|
+
return deviating;
|
|
5334
|
+
}
|
|
5335
|
+
function findDominantPattern(counts) {
|
|
5336
|
+
let dominant = null;
|
|
5337
|
+
let dominantCount = 0;
|
|
5338
|
+
for (const [pattern, data] of counts) {
|
|
5339
|
+
if (data.count > dominantCount) {
|
|
5340
|
+
dominantCount = data.count;
|
|
5341
|
+
dominant = pattern;
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
if (!dominant) return null;
|
|
5345
|
+
return { dominant, dominantCount };
|
|
5346
|
+
}
|
|
5347
|
+
function analyzePatternDistribution(profiles, patternNames) {
|
|
5348
|
+
const counts = buildPatternDistribution(profiles);
|
|
5349
|
+
if (counts.size < 2) return null;
|
|
5350
|
+
const result = findDominantPattern(counts);
|
|
5351
|
+
if (!result) return null;
|
|
5352
|
+
const { dominant, dominantCount } = result;
|
|
5353
|
+
const totalFiles = profiles.length;
|
|
5354
|
+
const consistencyScore = Math.round(dominantCount / totalFiles * 100);
|
|
5355
|
+
const deviating = collectDeviatingFiles(counts, dominant, profiles, patternNames);
|
|
5183
5356
|
if (deviating.length === 0) return null;
|
|
5184
5357
|
return {
|
|
5185
5358
|
detector: "architectural_consistency",
|
|
@@ -5281,13 +5454,25 @@ function classifyName(name) {
|
|
|
5281
5454
|
if (/^[a-z][a-z0-9]*$/.test(name)) return "camelCase";
|
|
5282
5455
|
return null;
|
|
5283
5456
|
}
|
|
5457
|
+
function isIdiomaticGo(name, convention) {
|
|
5458
|
+
if (convention === "PascalCase" && /^[A-Z]/.test(name)) return true;
|
|
5459
|
+
if (/^(?:HTTP|URL|ID|JSON|XML|SQL|API|DNS|TCP|UDP|IP|TLS|SSH|EOF)/.test(name)) return true;
|
|
5460
|
+
return false;
|
|
5461
|
+
}
|
|
5462
|
+
function isIdiomaticJsTs(name, convention, symbolType) {
|
|
5463
|
+
if (symbolType === "class" && convention === "PascalCase") return true;
|
|
5464
|
+
if (symbolType === "function" && convention === "PascalCase" && /^[A-Z]\w*$/.test(name)) return true;
|
|
5465
|
+
return false;
|
|
5466
|
+
}
|
|
5467
|
+
function isIdiomaticPython(name, convention, symbolType) {
|
|
5468
|
+
if (symbolType === "class" && convention === "PascalCase") return true;
|
|
5469
|
+
if (name.startsWith("__") && name.endsWith("__")) return true;
|
|
5470
|
+
return false;
|
|
5471
|
+
}
|
|
5284
5472
|
function isIdiomatic(name, convention, symbolType, language) {
|
|
5285
|
-
if (language === "go" &&
|
|
5286
|
-
if (language === "
|
|
5287
|
-
if (
|
|
5288
|
-
if ((language === "javascript" || language === "typescript") && symbolType === "function" && convention === "PascalCase" && /^[A-Z]\w*$/.test(name)) return true;
|
|
5289
|
-
if (language === "python" && symbolType === "class" && convention === "PascalCase") return true;
|
|
5290
|
-
if (language === "python" && name.startsWith("__") && name.endsWith("__")) return true;
|
|
5473
|
+
if (language === "go" && isIdiomaticGo(name, convention)) return true;
|
|
5474
|
+
if ((language === "javascript" || language === "typescript") && isIdiomaticJsTs(name, convention, symbolType)) return true;
|
|
5475
|
+
if (language === "python" && isIdiomaticPython(name, convention, symbolType)) return true;
|
|
5291
5476
|
if (symbolType === "constant" && convention === "SCREAMING_SNAKE") return true;
|
|
5292
5477
|
return false;
|
|
5293
5478
|
}
|
|
@@ -5382,6 +5567,68 @@ function analyzeFileNaming(files) {
|
|
|
5382
5567
|
recommendation: `Standardize file names to ${dominant}. ${deviating.length} files use a different convention.`
|
|
5383
5568
|
};
|
|
5384
5569
|
}
|
|
5570
|
+
function collectDeviantFiles(convCounts, dominant) {
|
|
5571
|
+
const fileDeviants = /* @__PURE__ */ new Map();
|
|
5572
|
+
for (const [conv, syms] of convCounts) {
|
|
5573
|
+
if (conv === dominant) continue;
|
|
5574
|
+
for (const s of syms) {
|
|
5575
|
+
if (!fileDeviants.has(s.file)) fileDeviants.set(s.file, []);
|
|
5576
|
+
fileDeviants.get(s.file).push(s);
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5579
|
+
const deviatingFiles = [];
|
|
5580
|
+
for (const [filePath, syms] of fileDeviants) {
|
|
5581
|
+
const uniqueConventions = [...new Set(syms.map((s) => s.convention))];
|
|
5582
|
+
deviatingFiles.push({
|
|
5583
|
+
path: filePath,
|
|
5584
|
+
detectedPattern: uniqueConventions.join(", "),
|
|
5585
|
+
evidence: syms.slice(0, 3).map((s) => ({ line: s.line, code: s.name }))
|
|
5586
|
+
});
|
|
5587
|
+
}
|
|
5588
|
+
return deviatingFiles;
|
|
5589
|
+
}
|
|
5590
|
+
function buildConventionFinding(type, dominant, maxCount, totalSymbols, deviatingFiles) {
|
|
5591
|
+
const deviantCount = totalSymbols - maxCount;
|
|
5592
|
+
const consistencyScore = Math.round(maxCount / totalSymbols * 100);
|
|
5593
|
+
return {
|
|
5594
|
+
detector: "naming_conventions",
|
|
5595
|
+
subCategory: `${type}_names`,
|
|
5596
|
+
driftCategory: "naming_conventions",
|
|
5597
|
+
severity: deviatingFiles.length > 5 ? "error" : "warning",
|
|
5598
|
+
confidence: 0.8,
|
|
5599
|
+
finding: `${type} naming convention oscillates: ${maxCount} use ${dominant}, ${deviantCount} use other conventions \u2014 likely from different AI sessions`,
|
|
5600
|
+
dominantPattern: dominant,
|
|
5601
|
+
dominantCount: maxCount,
|
|
5602
|
+
totalRelevantFiles: totalSymbols,
|
|
5603
|
+
consistencyScore,
|
|
5604
|
+
deviatingFiles: deviatingFiles.slice(0, 10),
|
|
5605
|
+
recommendation: `${maxCount} of ${totalSymbols} ${type} names use ${dominant}. Standardize deviating names.`
|
|
5606
|
+
};
|
|
5607
|
+
}
|
|
5608
|
+
function analyzeSymbolTypeConventions(type, typeSymbols) {
|
|
5609
|
+
if (typeSymbols.length < 3) return null;
|
|
5610
|
+
const convCounts = /* @__PURE__ */ new Map();
|
|
5611
|
+
for (const s of typeSymbols) {
|
|
5612
|
+
if (!convCounts.has(s.convention)) convCounts.set(s.convention, []);
|
|
5613
|
+
convCounts.get(s.convention).push(s);
|
|
5614
|
+
}
|
|
5615
|
+
if (convCounts.size < 2) return null;
|
|
5616
|
+
let dominant = null;
|
|
5617
|
+
let maxCount = 0;
|
|
5618
|
+
for (const [conv, syms] of convCounts) {
|
|
5619
|
+
if (syms.length > maxCount) {
|
|
5620
|
+
maxCount = syms.length;
|
|
5621
|
+
dominant = conv;
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5624
|
+
if (!dominant) return null;
|
|
5625
|
+
const totalSymbols = typeSymbols.length;
|
|
5626
|
+
const deviantCount = totalSymbols - maxCount;
|
|
5627
|
+
if (deviantCount < 3 || deviantCount / totalSymbols < 0.1) return null;
|
|
5628
|
+
const deviatingFiles = collectDeviantFiles(convCounts, dominant);
|
|
5629
|
+
if (deviatingFiles.length < 2) return null;
|
|
5630
|
+
return buildConventionFinding(type, dominant, maxCount, totalSymbols, deviatingFiles);
|
|
5631
|
+
}
|
|
5385
5632
|
var conventionOscillation = {
|
|
5386
5633
|
id: "convention-oscillation",
|
|
5387
5634
|
name: "Naming Convention Oscillation",
|
|
@@ -5396,62 +5643,8 @@ var conventionOscillation = {
|
|
|
5396
5643
|
const symbolTypes = [...new Set(allSymbols.map((s) => s.symbolType))];
|
|
5397
5644
|
for (const type of symbolTypes) {
|
|
5398
5645
|
const typeSymbols = allSymbols.filter((s) => s.symbolType === type);
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
for (const s of typeSymbols) {
|
|
5402
|
-
if (!convCounts.has(s.convention)) convCounts.set(s.convention, []);
|
|
5403
|
-
convCounts.get(s.convention).push(s);
|
|
5404
|
-
}
|
|
5405
|
-
if (convCounts.size < 2) continue;
|
|
5406
|
-
let dominant = null;
|
|
5407
|
-
let maxCount = 0;
|
|
5408
|
-
for (const [conv, syms] of convCounts) {
|
|
5409
|
-
if (syms.length > maxCount) {
|
|
5410
|
-
maxCount = syms.length;
|
|
5411
|
-
dominant = conv;
|
|
5412
|
-
}
|
|
5413
|
-
}
|
|
5414
|
-
if (!dominant) continue;
|
|
5415
|
-
const totalSymbols = typeSymbols.length;
|
|
5416
|
-
const consistencyScore = Math.round(maxCount / totalSymbols * 100);
|
|
5417
|
-
const deviantCount = totalSymbols - maxCount;
|
|
5418
|
-
if (deviantCount < 3 || deviantCount / totalSymbols < 0.1) continue;
|
|
5419
|
-
const fileDeviants = /* @__PURE__ */ new Map();
|
|
5420
|
-
for (const [conv, syms] of convCounts) {
|
|
5421
|
-
if (conv === dominant) continue;
|
|
5422
|
-
for (const s of syms) {
|
|
5423
|
-
if (!fileDeviants.has(s.file)) fileDeviants.set(s.file, []);
|
|
5424
|
-
fileDeviants.get(s.file).push(s);
|
|
5425
|
-
}
|
|
5426
|
-
}
|
|
5427
|
-
const deviatingFiles = [];
|
|
5428
|
-
for (const [filePath, syms] of fileDeviants) {
|
|
5429
|
-
const uniqueConventions = [...new Set(syms.map((s) => s.convention))];
|
|
5430
|
-
deviatingFiles.push({
|
|
5431
|
-
path: filePath,
|
|
5432
|
-
detectedPattern: uniqueConventions.join(", "),
|
|
5433
|
-
evidence: syms.slice(0, 3).map((s) => ({ line: s.line, code: s.name }))
|
|
5434
|
-
});
|
|
5435
|
-
}
|
|
5436
|
-
if (deviatingFiles.length < 2) continue;
|
|
5437
|
-
const distribution = {};
|
|
5438
|
-
for (const [conv, syms] of convCounts) {
|
|
5439
|
-
distribution[conv] = syms.length;
|
|
5440
|
-
}
|
|
5441
|
-
findings.push({
|
|
5442
|
-
detector: "naming_conventions",
|
|
5443
|
-
subCategory: `${type}_names`,
|
|
5444
|
-
driftCategory: "naming_conventions",
|
|
5445
|
-
severity: deviatingFiles.length > 5 ? "error" : "warning",
|
|
5446
|
-
confidence: 0.8,
|
|
5447
|
-
finding: `${type} naming convention oscillates: ${maxCount} use ${dominant}, ${deviantCount} use other conventions \u2014 likely from different AI sessions`,
|
|
5448
|
-
dominantPattern: dominant,
|
|
5449
|
-
dominantCount: maxCount,
|
|
5450
|
-
totalRelevantFiles: totalSymbols,
|
|
5451
|
-
consistencyScore,
|
|
5452
|
-
deviatingFiles: deviatingFiles.slice(0, 10),
|
|
5453
|
-
recommendation: `${maxCount} of ${totalSymbols} ${type} names use ${dominant}. Standardize deviating names.`
|
|
5454
|
-
});
|
|
5646
|
+
const finding = analyzeSymbolTypeConventions(type, typeSymbols);
|
|
5647
|
+
if (finding) findings.push(finding);
|
|
5455
5648
|
}
|
|
5456
5649
|
const fileNaming = analyzeFileNaming(ctx.files);
|
|
5457
5650
|
if (fileNaming) findings.push(fileNaming);
|
|
@@ -5666,33 +5859,48 @@ function isInterfaceImplPair(a, b) {
|
|
|
5666
5859
|
const bIsInterface = /(?:interface|store|contract|abstract|base|types)\./i.test(b.file) || b.bodyTokenCount < 10;
|
|
5667
5860
|
return aIsInterface && !bIsInterface || !aIsInterface && bIsInterface;
|
|
5668
5861
|
}
|
|
5669
|
-
function
|
|
5862
|
+
function isConventionalName(name) {
|
|
5863
|
+
return CONVENTION_NAMES.has(name.toLowerCase());
|
|
5864
|
+
}
|
|
5865
|
+
function areStructurallySimilar(a, b) {
|
|
5670
5866
|
if (a.domainCategory !== b.domainCategory || a.domainCategory === "general") return false;
|
|
5671
5867
|
if (a.file === b.file) return false;
|
|
5672
5868
|
if (Math.abs(a.paramCount - b.paramCount) > 1) return false;
|
|
5673
5869
|
const sizeRatio = Math.min(a.bodyTokenCount, b.bodyTokenCount) / Math.max(a.bodyTokenCount, b.bodyTokenCount);
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
if (
|
|
5678
|
-
if (nameA
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5870
|
+
return sizeRatio >= 0.4;
|
|
5871
|
+
}
|
|
5872
|
+
function areNamesSimilar(nameA, nameB, hashA, hashB, tokenCountA) {
|
|
5873
|
+
if (nameA === nameB) return true;
|
|
5874
|
+
if (nameA.length >= 6 && nameB.length >= 6 && levenshtein(nameA, nameB) <= 3) return true;
|
|
5875
|
+
if (hashA === hashB && tokenCountA > 20) return true;
|
|
5876
|
+
return false;
|
|
5877
|
+
}
|
|
5878
|
+
function isLayerPair(a, b) {
|
|
5682
5879
|
const aIsRepo = /(?:repository|repo|store|postgres|mysql|mongo|dal)/i.test(a.file);
|
|
5683
5880
|
const bIsRepo = /(?:repository|repo|store|postgres|mysql|mongo|dal)/i.test(b.file);
|
|
5684
5881
|
const aIsHandler = /(?:handler|controller|route|endpoint|api)/i.test(a.file);
|
|
5685
5882
|
const bIsHandler = /(?:handler|controller|route|endpoint|api)/i.test(b.file);
|
|
5686
|
-
if (aIsHandler && bIsRepo || aIsRepo && bIsHandler) return
|
|
5883
|
+
if (aIsHandler && bIsRepo || aIsRepo && bIsHandler) return true;
|
|
5884
|
+
const nameA = a.name.toLowerCase();
|
|
5885
|
+
const nameB = b.name.toLowerCase();
|
|
5687
5886
|
if (nameA === nameB && a.paramCount === b.paramCount) {
|
|
5688
|
-
if (aIsHandler && bIsHandler) return
|
|
5689
|
-
if (aIsRepo && bIsRepo) return
|
|
5887
|
+
if (aIsHandler && bIsHandler) return true;
|
|
5888
|
+
if (aIsRepo && bIsRepo) return true;
|
|
5690
5889
|
}
|
|
5691
|
-
if (nameA === nameB) return true;
|
|
5692
|
-
if (nameA.length >= 6 && nameB.length >= 6 && levenshtein(nameA, nameB) <= 3) return true;
|
|
5693
|
-
if (a.bodyHash === b.bodyHash && a.bodyTokenCount > 20) return true;
|
|
5694
5890
|
return false;
|
|
5695
5891
|
}
|
|
5892
|
+
function areSemanticallySimlar(a, b) {
|
|
5893
|
+
if (!areStructurallySimilar(a, b)) return false;
|
|
5894
|
+
const nameA = a.name.toLowerCase();
|
|
5895
|
+
const nameB = b.name.toLowerCase();
|
|
5896
|
+
if (isConventionalName(nameA) || isConventionalName(nameB)) return false;
|
|
5897
|
+
if (nameA === nameB && /(?:handler|controller|resource|api)/i.test(a.file) && /(?:handler|controller|resource|api)/i.test(b.file)) {
|
|
5898
|
+
if (["list", "get", "create", "update", "delete", "find", "search"].includes(nameA)) return false;
|
|
5899
|
+
}
|
|
5900
|
+
if (nameA === nameB && isInterfaceImplPair(a, b)) return false;
|
|
5901
|
+
if (isLayerPair(a, b)) return false;
|
|
5902
|
+
return areNamesSimilar(nameA, nameB, a.bodyHash, b.bodyHash, a.bodyTokenCount);
|
|
5903
|
+
}
|
|
5696
5904
|
function levenshtein(a, b) {
|
|
5697
5905
|
const m = a.length, n = b.length;
|
|
5698
5906
|
if (m === 0) return n;
|
|
@@ -5983,8 +6191,7 @@ function detectUnroutedHandlers(allExports, allRoutes, usage) {
|
|
|
5983
6191
|
}
|
|
5984
6192
|
return findings;
|
|
5985
6193
|
}
|
|
5986
|
-
function
|
|
5987
|
-
const findings = [];
|
|
6194
|
+
function extractTypeDefinitions(files) {
|
|
5988
6195
|
const typeDefinitions = [];
|
|
5989
6196
|
const goMethodReceivers = /* @__PURE__ */ new Set();
|
|
5990
6197
|
for (const file of files) {
|
|
@@ -6014,6 +6221,9 @@ function detectUnusedTypeScaffolding(files, usage) {
|
|
|
6014
6221
|
typeDefinitions.push({ name: typeName, file: file.path, line: i + 1 });
|
|
6015
6222
|
}
|
|
6016
6223
|
}
|
|
6224
|
+
return typeDefinitions;
|
|
6225
|
+
}
|
|
6226
|
+
function findUnusedTypes(typeDefinitions, usage) {
|
|
6017
6227
|
const unusedTypesByFile = /* @__PURE__ */ new Map();
|
|
6018
6228
|
for (const td of typeDefinitions) {
|
|
6019
6229
|
const refs = usage.get(td.name);
|
|
@@ -6023,6 +6233,12 @@ function detectUnusedTypeScaffolding(files, usage) {
|
|
|
6023
6233
|
unusedTypesByFile.get(td.file).push(td);
|
|
6024
6234
|
}
|
|
6025
6235
|
}
|
|
6236
|
+
return unusedTypesByFile;
|
|
6237
|
+
}
|
|
6238
|
+
function detectUnusedTypeScaffolding(files, usage) {
|
|
6239
|
+
const findings = [];
|
|
6240
|
+
const typeDefinitions = extractTypeDefinitions(files);
|
|
6241
|
+
const unusedTypesByFile = findUnusedTypes(typeDefinitions, usage);
|
|
6026
6242
|
for (const [filePath, unusedTypes] of unusedTypesByFile) {
|
|
6027
6243
|
if (unusedTypes.length < 3) continue;
|
|
6028
6244
|
findings.push({
|
|
@@ -6465,25 +6681,8 @@ function scoreColorFn(score, max) {
|
|
|
6465
6681
|
if (ratio >= 0.5) return chalk.yellow;
|
|
6466
6682
|
return chalk.red;
|
|
6467
6683
|
}
|
|
6468
|
-
function
|
|
6684
|
+
function renderFindingsList(result) {
|
|
6469
6685
|
const lines = [];
|
|
6470
|
-
const banner = ` VibeDrift v${getVersion()} `;
|
|
6471
|
-
const border = "\u2500".repeat(banner.length);
|
|
6472
|
-
lines.push(chalk.cyan(`\u256D${border}\u256E`));
|
|
6473
|
-
lines.push(chalk.cyan(`\u2502${banner}\u2502`));
|
|
6474
|
-
lines.push(chalk.cyan(`\u2570${border}\u256F`));
|
|
6475
|
-
lines.push("");
|
|
6476
|
-
const langCounts = /* @__PURE__ */ new Map();
|
|
6477
|
-
for (const f of result.context.files) {
|
|
6478
|
-
if (f.language) {
|
|
6479
|
-
const label = getLanguageDisplayName(f.language);
|
|
6480
|
-
langCounts.set(label, (langCounts.get(label) ?? 0) + 1);
|
|
6481
|
-
}
|
|
6482
|
-
}
|
|
6483
|
-
const langStr = [...langCounts.entries()].map(([l, c]) => `${l}: ${c}`).join(", ");
|
|
6484
|
-
lines.push(chalk.dim(`Scanning: ${result.context.rootDir}`));
|
|
6485
|
-
lines.push(chalk.dim(`Files: ${result.context.files.length} (${langStr}) | Lines: ${result.context.totalLines.toLocaleString()} | Time: ${(result.scanTimeMs / 1e3).toFixed(1)}s`));
|
|
6486
|
-
lines.push("");
|
|
6487
6686
|
for (const cat of ALL_CATEGORIES) {
|
|
6488
6687
|
const config = CATEGORY_CONFIG[cat];
|
|
6489
6688
|
const s = result.scores[cat];
|
|
@@ -6517,6 +6716,10 @@ function renderTerminalOutput(result) {
|
|
|
6517
6716
|
}
|
|
6518
6717
|
lines.push("");
|
|
6519
6718
|
}
|
|
6719
|
+
return lines;
|
|
6720
|
+
}
|
|
6721
|
+
function renderCategoryBars(result) {
|
|
6722
|
+
const lines = [];
|
|
6520
6723
|
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"));
|
|
6521
6724
|
for (const cat of ALL_CATEGORIES) {
|
|
6522
6725
|
const config = CATEGORY_CONFIG[cat];
|
|
@@ -6540,6 +6743,35 @@ function renderTerminalOutput(result) {
|
|
|
6540
6743
|
const totalColor = scoreColorFn(result.compositeScore, result.maxCompositeScore);
|
|
6541
6744
|
lines.push(` ${padRight("Total Score:", 28)} ${totalColor(`${result.compositeScore.toFixed(0)}/${result.maxCompositeScore}`)}`);
|
|
6542
6745
|
lines.push("");
|
|
6746
|
+
return lines;
|
|
6747
|
+
}
|
|
6748
|
+
function renderScoreSection(result) {
|
|
6749
|
+
const lines = [];
|
|
6750
|
+
const banner = ` VibeDrift v${getVersion()} `;
|
|
6751
|
+
const border = "\u2500".repeat(banner.length);
|
|
6752
|
+
lines.push(chalk.cyan(`\u256D${border}\u256E`));
|
|
6753
|
+
lines.push(chalk.cyan(`\u2502${banner}\u2502`));
|
|
6754
|
+
lines.push(chalk.cyan(`\u2570${border}\u256F`));
|
|
6755
|
+
lines.push("");
|
|
6756
|
+
const langCounts = /* @__PURE__ */ new Map();
|
|
6757
|
+
for (const f of result.context.files) {
|
|
6758
|
+
if (f.language) {
|
|
6759
|
+
const label = getLanguageDisplayName(f.language);
|
|
6760
|
+
langCounts.set(label, (langCounts.get(label) ?? 0) + 1);
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
const langStr = [...langCounts.entries()].map(([l, c]) => `${l}: ${c}`).join(", ");
|
|
6764
|
+
lines.push(chalk.dim(`Scanning: ${result.context.rootDir}`));
|
|
6765
|
+
lines.push(chalk.dim(`Files: ${result.context.files.length} (${langStr}) | Lines: ${result.context.totalLines.toLocaleString()} | Time: ${(result.scanTimeMs / 1e3).toFixed(1)}s`));
|
|
6766
|
+
lines.push("");
|
|
6767
|
+
return lines;
|
|
6768
|
+
}
|
|
6769
|
+
function renderTerminalOutput(result) {
|
|
6770
|
+
const lines = [
|
|
6771
|
+
...renderScoreSection(result),
|
|
6772
|
+
...renderFindingsList(result),
|
|
6773
|
+
...renderCategoryBars(result)
|
|
6774
|
+
];
|
|
6543
6775
|
if (result.deepInsights.length > 0) {
|
|
6544
6776
|
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"));
|
|
6545
6777
|
for (const insight of result.deepInsights.slice(0, 10)) {
|
|
@@ -6673,8 +6905,7 @@ function extractIntentPatterns(result) {
|
|
|
6673
6905
|
}
|
|
6674
6906
|
return patterns;
|
|
6675
6907
|
}
|
|
6676
|
-
function
|
|
6677
|
-
const entries = [...result.perFileScores.entries()];
|
|
6908
|
+
function buildDriftFileMap(result) {
|
|
6678
6909
|
const driftFiles = /* @__PURE__ */ new Map();
|
|
6679
6910
|
for (const d of result.driftFindings ?? []) {
|
|
6680
6911
|
for (const df of d.deviatingFiles) {
|
|
@@ -6683,6 +6914,9 @@ function buildFileCoherence(result) {
|
|
|
6683
6914
|
driftFiles.get(df.path).push({ category: catKey, pattern: df.detectedPattern });
|
|
6684
6915
|
}
|
|
6685
6916
|
}
|
|
6917
|
+
return driftFiles;
|
|
6918
|
+
}
|
|
6919
|
+
function buildCategorySet(result) {
|
|
6686
6920
|
const subCatNames = {
|
|
6687
6921
|
data_access: "Data Access",
|
|
6688
6922
|
error_handling: "Error Handling",
|
|
@@ -6703,9 +6937,12 @@ function buildFileCoherence(result) {
|
|
|
6703
6937
|
catSet.set(d.driftCategory, baseCatNames[d.driftCategory]);
|
|
6704
6938
|
}
|
|
6705
6939
|
}
|
|
6706
|
-
|
|
6707
|
-
|
|
6940
|
+
return catSet;
|
|
6941
|
+
}
|
|
6942
|
+
function buildCodeDnaPatternData(result) {
|
|
6708
6943
|
const filePatternMap = /* @__PURE__ */ new Map();
|
|
6944
|
+
let projectDominantPattern = "";
|
|
6945
|
+
const dna = result.codeDnaResult;
|
|
6709
6946
|
if (dna?.patternDistributions?.length > 0) {
|
|
6710
6947
|
const patternCounts = /* @__PURE__ */ new Map();
|
|
6711
6948
|
for (const pd of dna.patternDistributions) {
|
|
@@ -6721,9 +6958,16 @@ function buildFileCoherence(result) {
|
|
|
6721
6958
|
projectDominantPattern = p;
|
|
6722
6959
|
}
|
|
6723
6960
|
}
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6961
|
+
}
|
|
6962
|
+
return { projectDominantPattern, filePatternMap };
|
|
6963
|
+
}
|
|
6964
|
+
function buildFileCoherence(result) {
|
|
6965
|
+
const entries = [...result.perFileScores.entries()];
|
|
6966
|
+
const driftFiles = buildDriftFileMap(result);
|
|
6967
|
+
const catSet = buildCategorySet(result);
|
|
6968
|
+
const { projectDominantPattern, filePatternMap } = buildCodeDnaPatternData(result);
|
|
6969
|
+
if (projectDominantPattern) {
|
|
6970
|
+
catSet.set("codedna_architecture", "Architecture");
|
|
6727
6971
|
}
|
|
6728
6972
|
const categories = [...catSet.values()];
|
|
6729
6973
|
const catKeys = [...catSet.keys()];
|
|
@@ -6796,9 +7040,7 @@ function buildCoherenceMatrix(files) {
|
|
|
6796
7040
|
const colHeaders = categories.map(
|
|
6797
7041
|
(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>`
|
|
6798
7042
|
).join("");
|
|
6799
|
-
function
|
|
6800
|
-
const pctColor = f.alignmentPct >= 100 ? "var(--intent-green)" : f.alignmentPct >= 50 ? "var(--drift-amber)" : "var(--drift-red)";
|
|
6801
|
-
const bgTint = f.alignmentPct < 50 ? "var(--tint-red)" : f.alignmentPct < 100 ? "var(--tint-amber)" : "transparent";
|
|
7043
|
+
function buildAlignmentCells(f) {
|
|
6802
7044
|
const deviations = [];
|
|
6803
7045
|
const cells = f.alignments.map((a) => {
|
|
6804
7046
|
if (a.matches) return `<td style="text-align:center;padding:6px 12px;color:var(--intent-green);font-size:14px">●</td>`;
|
|
@@ -6806,7 +7048,16 @@ function buildCoherenceMatrix(files) {
|
|
|
6806
7048
|
deviations.push(`${a.category}: ${a.actual ?? "deviates"}`);
|
|
6807
7049
|
return `<td style="text-align:center;padding:6px 12px;color:${devColor};font-size:13px" data-tooltip="${esc(a.actual ?? "deviates")}">◆</td>`;
|
|
6808
7050
|
}).join("");
|
|
6809
|
-
|
|
7051
|
+
return { cells, deviations };
|
|
7052
|
+
}
|
|
7053
|
+
function buildDeviationSummary(deviations) {
|
|
7054
|
+
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>`;
|
|
7055
|
+
}
|
|
7056
|
+
function fileRow(f, open) {
|
|
7057
|
+
const pctColor = f.alignmentPct >= 100 ? "var(--intent-green)" : f.alignmentPct >= 50 ? "var(--drift-amber)" : "var(--drift-red)";
|
|
7058
|
+
const bgTint = f.alignmentPct < 50 ? "var(--tint-red)" : f.alignmentPct < 100 ? "var(--tint-amber)" : "transparent";
|
|
7059
|
+
const { cells, deviations } = buildAlignmentCells(f);
|
|
7060
|
+
const summary = buildDeviationSummary(deviations);
|
|
6810
7061
|
return `<tr style="background:${bgTint}" id="file-${esc(f.path.replace(/[^a-zA-Z0-9]/g, "-"))}">
|
|
6811
7062
|
<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>
|
|
6812
7063
|
${cells}
|
|
@@ -7069,6 +7320,66 @@ function buildFixFirst(result) {
|
|
|
7069
7320
|
${cards}
|
|
7070
7321
|
</section>`;
|
|
7071
7322
|
}
|
|
7323
|
+
function buildDeviatingBlocks(d) {
|
|
7324
|
+
const isConvention = d.driftCategory === "naming_conventions";
|
|
7325
|
+
const devByPattern = /* @__PURE__ */ new Map();
|
|
7326
|
+
for (const df of d.deviatingFiles) {
|
|
7327
|
+
const key = df.detectedPattern;
|
|
7328
|
+
if (!devByPattern.has(key)) devByPattern.set(key, []);
|
|
7329
|
+
devByPattern.get(key).push(df);
|
|
7330
|
+
}
|
|
7331
|
+
if (isConvention && d.deviatingFiles.length > 4) {
|
|
7332
|
+
return [...devByPattern.entries()].map(([pattern, files]) => {
|
|
7333
|
+
const fileList = files.map((df) => esc(df.path)).join("</div><div style='padding:1px 0'>");
|
|
7334
|
+
const collapseId = `conv-${pattern.replace(/[^a-zA-Z0-9]/g, "-")}-${Math.random().toString(36).slice(2, 6)}`;
|
|
7335
|
+
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7336
|
+
<div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT — ${esc(pattern)} — ${files.length} files</div>
|
|
7337
|
+
<div style="cursor:pointer;font-size:12px;color:var(--text-secondary);margin-top:6px" data-collapse="${collapseId}">▶ Show ${files.length} files</div>
|
|
7338
|
+
<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>
|
|
7339
|
+
</div>`;
|
|
7340
|
+
}).join("");
|
|
7341
|
+
}
|
|
7342
|
+
return d.deviatingFiles.slice(0, 4).map((df) => {
|
|
7343
|
+
const evidence = df.evidence.slice(0, 3).map(
|
|
7344
|
+
(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>`
|
|
7345
|
+
).join("");
|
|
7346
|
+
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7347
|
+
<div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT — ${esc(df.detectedPattern)}</div>
|
|
7348
|
+
<div class="mono" style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">${esc(df.path)}</div>
|
|
7349
|
+
${evidence}
|
|
7350
|
+
</div>`;
|
|
7351
|
+
}).join("");
|
|
7352
|
+
}
|
|
7353
|
+
function buildDriftRecommendation(d) {
|
|
7354
|
+
let recText = d.recommendation ?? "";
|
|
7355
|
+
if (recText && d.deviatingFiles.length > 0) {
|
|
7356
|
+
const firstDev = d.deviatingFiles[0];
|
|
7357
|
+
const firstEvidence = firstDev.evidence?.[0];
|
|
7358
|
+
if (recText.includes("Migrate deviating files") || recText.includes("Standardize deviating")) {
|
|
7359
|
+
const fileList = d.deviatingFiles.slice(0, 3).map((f) => f.path.split("/").pop()).join(", ");
|
|
7360
|
+
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).`;
|
|
7361
|
+
if (firstEvidence) {
|
|
7362
|
+
recText += ` Example: ${firstDev.path.split("/").pop()}:${firstEvidence.line} currently uses ${firstDev.detectedPattern}.`;
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
}
|
|
7366
|
+
return recText;
|
|
7367
|
+
}
|
|
7368
|
+
function buildDistributionBar(d) {
|
|
7369
|
+
const domPct = d.totalRelevantFiles > 0 ? Math.round(d.dominantCount / d.totalRelevantFiles * 100) : 0;
|
|
7370
|
+
const devPct = 100 - domPct;
|
|
7371
|
+
return `<div style="margin:12px 0">
|
|
7372
|
+
<div style="display:flex;height:6px;border-radius:0;overflow:hidden;gap:2px">
|
|
7373
|
+
<div style="width:${domPct}%;background:var(--intent-green);border-radius:0"></div>
|
|
7374
|
+
<div style="width:${devPct}%;background:var(--drift-orange);border-radius:0"></div>
|
|
7375
|
+
</div>
|
|
7376
|
+
<div style="display:flex;gap:16px;margin-top:4px;font-size:11px;color:var(--text-tertiary)">
|
|
7377
|
+
<span>${esc(d.dominantPattern)} (${d.dominantCount})</span>
|
|
7378
|
+
<span>Deviating (${d.totalRelevantFiles - d.dominantCount})</span>
|
|
7379
|
+
<span style="margin-left:auto">${d.consistencyScore}% consistent</span>
|
|
7380
|
+
</div>
|
|
7381
|
+
</div>`;
|
|
7382
|
+
}
|
|
7072
7383
|
function buildDriftFindings(result) {
|
|
7073
7384
|
const driftCats = ["architectural_consistency", "security_posture", "semantic_duplication", "naming_conventions", "phantom_scaffolding"];
|
|
7074
7385
|
const catLabels = {
|
|
@@ -7092,61 +7403,9 @@ function buildDriftFindings(result) {
|
|
|
7092
7403
|
const domBlock = `<div style="background:var(--tint-green);border-left:3px solid var(--intent-green);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7093
7404
|
<div class="label" style="color:var(--intent-green);margin-bottom:6px">INTENT (DOMINANT) — ${esc(d.dominantPattern)} — ${d.dominantCount} files${closeSplitNote}</div>
|
|
7094
7405
|
</div>`;
|
|
7095
|
-
const
|
|
7096
|
-
const
|
|
7097
|
-
|
|
7098
|
-
const key = df.detectedPattern;
|
|
7099
|
-
if (!devByPattern.has(key)) devByPattern.set(key, []);
|
|
7100
|
-
devByPattern.get(key).push(df);
|
|
7101
|
-
}
|
|
7102
|
-
let devBlocks;
|
|
7103
|
-
if (isConvention && d.deviatingFiles.length > 4) {
|
|
7104
|
-
devBlocks = [...devByPattern.entries()].map(([pattern, files]) => {
|
|
7105
|
-
const fileList = files.map((df) => esc(df.path)).join("</div><div style='padding:1px 0'>");
|
|
7106
|
-
const collapseId = `conv-${pattern.replace(/[^a-zA-Z0-9]/g, "-")}-${Math.random().toString(36).slice(2, 6)}`;
|
|
7107
|
-
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7108
|
-
<div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT — ${esc(pattern)} — ${files.length} files</div>
|
|
7109
|
-
<div style="cursor:pointer;font-size:12px;color:var(--text-secondary);margin-top:6px" data-collapse="${collapseId}">▶ Show ${files.length} files</div>
|
|
7110
|
-
<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>
|
|
7111
|
-
</div>`;
|
|
7112
|
-
}).join("");
|
|
7113
|
-
} else {
|
|
7114
|
-
devBlocks = d.deviatingFiles.slice(0, 4).map((df) => {
|
|
7115
|
-
const evidence = df.evidence.slice(0, 3).map(
|
|
7116
|
-
(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>`
|
|
7117
|
-
).join("");
|
|
7118
|
-
return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
|
|
7119
|
-
<div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT — ${esc(df.detectedPattern)}</div>
|
|
7120
|
-
<div class="mono" style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">${esc(df.path)}</div>
|
|
7121
|
-
${evidence}
|
|
7122
|
-
</div>`;
|
|
7123
|
-
}).join("");
|
|
7124
|
-
}
|
|
7125
|
-
const domPct = d.totalRelevantFiles > 0 ? Math.round(d.dominantCount / d.totalRelevantFiles * 100) : 0;
|
|
7126
|
-
const devPct = 100 - domPct;
|
|
7127
|
-
const distBar = `<div style="margin:12px 0">
|
|
7128
|
-
<div style="display:flex;height:6px;border-radius:0;overflow:hidden;gap:2px">
|
|
7129
|
-
<div style="width:${domPct}%;background:var(--intent-green);border-radius:0"></div>
|
|
7130
|
-
<div style="width:${devPct}%;background:var(--drift-orange);border-radius:0"></div>
|
|
7131
|
-
</div>
|
|
7132
|
-
<div style="display:flex;gap:16px;margin-top:4px;font-size:11px;color:var(--text-tertiary)">
|
|
7133
|
-
<span>${esc(d.dominantPattern)} (${d.dominantCount})</span>
|
|
7134
|
-
<span>Deviating (${d.totalRelevantFiles - d.dominantCount})</span>
|
|
7135
|
-
<span style="margin-left:auto">${d.consistencyScore}% consistent</span>
|
|
7136
|
-
</div>
|
|
7137
|
-
</div>`;
|
|
7138
|
-
let recText = d.recommendation ?? "";
|
|
7139
|
-
if (recText && d.deviatingFiles.length > 0) {
|
|
7140
|
-
const firstDev = d.deviatingFiles[0];
|
|
7141
|
-
const firstEvidence = firstDev.evidence?.[0];
|
|
7142
|
-
if (recText.includes("Migrate deviating files") || recText.includes("Standardize deviating")) {
|
|
7143
|
-
const fileList = d.deviatingFiles.slice(0, 3).map((f) => f.path.split("/").pop()).join(", ");
|
|
7144
|
-
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).`;
|
|
7145
|
-
if (firstEvidence) {
|
|
7146
|
-
recText += ` Example: ${firstDev.path.split("/").pop()}:${firstEvidence.line} currently uses ${firstDev.detectedPattern}.`;
|
|
7147
|
-
}
|
|
7148
|
-
}
|
|
7149
|
-
}
|
|
7406
|
+
const devBlocks = buildDeviatingBlocks(d);
|
|
7407
|
+
const distBar = buildDistributionBar(d);
|
|
7408
|
+
const recText = buildDriftRecommendation(d);
|
|
7150
7409
|
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)">
|
|
7151
7410
|
<strong>${esc(d.dominantPattern)}</strong> is dominant at ${domPctVal}%, but the split is close.
|
|
7152
7411
|
If your team prefers <strong>${esc(d.deviatingFiles[0]?.detectedPattern ?? "the alternative")}</strong>, adopt that instead.
|
|
@@ -7374,6 +7633,37 @@ function buildDeepInsights(result) {
|
|
|
7374
7633
|
${cards}
|
|
7375
7634
|
</section>`;
|
|
7376
7635
|
}
|
|
7636
|
+
function buildSequenceCards(similarities) {
|
|
7637
|
+
const seqCards = similarities.slice(0, 6).map((sim) => {
|
|
7638
|
+
const pct = Math.round(sim.similarity * 100);
|
|
7639
|
+
const color = pct >= 90 ? "var(--drift-red)" : "var(--drift-orange)";
|
|
7640
|
+
const matchLabel = pct >= 100 ? "Exact duplicate" : pct >= 90 ? "Near-exact match" : "Similar";
|
|
7641
|
+
return `<div style="display:flex;align-items:center;gap:12px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
|
|
7642
|
+
<span class="mono" style="min-width:40px;font-weight:700;color:${color}">${pct}%</span>
|
|
7643
|
+
<div style="flex:1">
|
|
7644
|
+
<span style="font-size:11px;font-weight:600;color:${color};margin-right:6px">${matchLabel}</span>
|
|
7645
|
+
<span class="mono" style="font-size:12px;color:var(--text-secondary)">${esc(sim.functionA.name)}()</span>
|
|
7646
|
+
<span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionA.relativePath || sim.functionA.file))}</span>
|
|
7647
|
+
<span style="color:var(--text-tertiary);margin:0 4px">↔</span>
|
|
7648
|
+
<span class="mono" style="font-size:12px;color:var(--drift-orange)">${esc(sim.functionB.name)}()</span>
|
|
7649
|
+
<span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionB.relativePath || sim.functionB.file))}</span>
|
|
7650
|
+
</div>
|
|
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> 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>`;
|
|
7654
|
+
}
|
|
7655
|
+
function buildDeviationCards(justifications) {
|
|
7656
|
+
const devCards = justifications.map((dj) => {
|
|
7657
|
+
const vColor = dj.verdict === "likely_justified" ? "var(--intent-green)" : dj.verdict === "likely_accidental" ? "var(--drift-red)" : "var(--drift-amber)";
|
|
7658
|
+
const vLabel = dj.verdict === "likely_justified" ? "JUSTIFIED" : dj.verdict === "likely_accidental" ? "ACCIDENTAL" : "UNCERTAIN";
|
|
7659
|
+
return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
|
|
7660
|
+
<span class="sev-badge" style="background:${vColor};min-width:80px;text-align:center">${vLabel}</span>
|
|
7661
|
+
<span class="mono" style="font-size:12px;color:var(--text-secondary);flex:1">${esc(shortPath(dj.relativePath || dj.file))}</span>
|
|
7662
|
+
<span style="font-size:12px;color:var(--text-tertiary)">${esc(dj.deviatingPattern)} vs ${esc(dj.dominantPattern)}</span>
|
|
7663
|
+
</div>`;
|
|
7664
|
+
}).join("");
|
|
7665
|
+
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>`;
|
|
7666
|
+
}
|
|
7377
7667
|
function buildCodeDnaSummary(result) {
|
|
7378
7668
|
const dna = result.codeDnaResult;
|
|
7379
7669
|
if (!dna) return "";
|
|
@@ -7384,35 +7674,10 @@ function buildCodeDnaSummary(result) {
|
|
|
7384
7674
|
if (!hasDupes && !hasSeqs && !hasTaint && !hasDevs) return "";
|
|
7385
7675
|
const parts = [];
|
|
7386
7676
|
if (hasSeqs) {
|
|
7387
|
-
|
|
7388
|
-
const pct = Math.round(sim.similarity * 100);
|
|
7389
|
-
const color = pct >= 90 ? "var(--drift-red)" : "var(--drift-orange)";
|
|
7390
|
-
const matchLabel = pct >= 100 ? "Exact duplicate" : pct >= 90 ? "Near-exact match" : "Similar";
|
|
7391
|
-
return `<div style="display:flex;align-items:center;gap:12px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
|
|
7392
|
-
<span class="mono" style="min-width:40px;font-weight:700;color:${color}">${pct}%</span>
|
|
7393
|
-
<div style="flex:1">
|
|
7394
|
-
<span style="font-size:11px;font-weight:600;color:${color};margin-right:6px">${matchLabel}</span>
|
|
7395
|
-
<span class="mono" style="font-size:12px;color:var(--text-secondary)">${esc(sim.functionA.name)}()</span>
|
|
7396
|
-
<span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionA.relativePath || sim.functionA.file))}</span>
|
|
7397
|
-
<span style="color:var(--text-tertiary);margin:0 4px">↔</span>
|
|
7398
|
-
<span class="mono" style="font-size:12px;color:var(--drift-orange)">${esc(sim.functionB.name)}()</span>
|
|
7399
|
-
<span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionB.relativePath || sim.functionB.file))}</span>
|
|
7400
|
-
</div>
|
|
7401
|
-
</div>`;
|
|
7402
|
-
}).join("");
|
|
7403
|
-
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>`);
|
|
7677
|
+
parts.push(buildSequenceCards(dna.sequenceSimilarities));
|
|
7404
7678
|
}
|
|
7405
7679
|
if (hasDevs) {
|
|
7406
|
-
|
|
7407
|
-
const vColor = dj.verdict === "likely_justified" ? "var(--intent-green)" : dj.verdict === "likely_accidental" ? "var(--drift-red)" : "var(--drift-amber)";
|
|
7408
|
-
const vLabel = dj.verdict === "likely_justified" ? "JUSTIFIED" : dj.verdict === "likely_accidental" ? "ACCIDENTAL" : "UNCERTAIN";
|
|
7409
|
-
return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
|
|
7410
|
-
<span class="sev-badge" style="background:${vColor};min-width:80px;text-align:center">${vLabel}</span>
|
|
7411
|
-
<span class="mono" style="font-size:12px;color:var(--text-secondary);flex:1">${esc(shortPath(dj.relativePath || dj.file))}</span>
|
|
7412
|
-
<span style="font-size:12px;color:var(--text-tertiary)">${esc(dj.deviatingPattern)} vs ${esc(dj.dominantPattern)}</span>
|
|
7413
|
-
</div>`;
|
|
7414
|
-
}).join("");
|
|
7415
|
-
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>`);
|
|
7680
|
+
parts.push(buildDeviationCards(dna.deviationJustifications));
|
|
7416
7681
|
}
|
|
7417
7682
|
if (parts.length === 0) return "";
|
|
7418
7683
|
const totalFindings = dna.findings?.length ?? 0;
|
|
@@ -7807,34 +8072,8 @@ function exportCSV() {
|
|
|
7807
8072
|
}
|
|
7808
8073
|
|
|
7809
8074
|
// \u2500\u2500\u2500\u2500 Export: DOCX (Word-compatible HTML) \u2500\u2500\u2500\u2500
|
|
7810
|
-
function
|
|
7811
|
-
|
|
7812
|
-
if (!d) { alert('Report data not available'); return; }
|
|
7813
|
-
|
|
7814
|
-
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>';
|
|
7815
|
-
html += 'body{font-family:Calibri,sans-serif;font-size:11pt;color:#222;margin:40px}';
|
|
7816
|
-
html += 'h1{font-size:20pt;color:#0B0F15;border-bottom:2px solid #00D4FF;padding-bottom:8px;margin-top:28px}';
|
|
7817
|
-
html += 'h2{font-size:14pt;color:#333;margin-top:20px}';
|
|
7818
|
-
html += 'h3{font-size:12pt;color:#555;margin-top:14px}';
|
|
7819
|
-
html += 'table{border-collapse:collapse;width:100%;margin:10px 0}';
|
|
7820
|
-
html += 'th,td{border:1px solid #ddd;padding:6px 10px;text-align:left;font-size:10pt}';
|
|
7821
|
-
html += 'th{background:#f0f4f8;font-weight:600}';
|
|
7822
|
-
html += '.sev-critical{color:#dc2626;font-weight:700} .sev-warning{color:#d97706;font-weight:700} .sev-info{color:#2563eb}';
|
|
7823
|
-
html += '.intent{background:#ecfdf5;border-left:3px solid #10b981;padding:8px 12px;margin:6px 0}';
|
|
7824
|
-
html += '.drift{background:#fff7ed;border-left:3px solid #f97316;padding:8px 12px;margin:6px 0}';
|
|
7825
|
-
html += '.rec{background:#f0f9ff;border-left:3px solid #0ea5e9;padding:8px 12px;margin:6px 0}';
|
|
7826
|
-
html += 'code{font-family:Consolas,monospace;font-size:9pt;background:#f5f5f5;padding:1px 4px}';
|
|
7827
|
-
html += '.page-break{page-break-before:always}';
|
|
7828
|
-
html += '</style></head><body>';
|
|
7829
|
-
|
|
7830
|
-
// Title
|
|
7831
|
-
html += '<div style="text-align:center;margin-bottom:30px">';
|
|
7832
|
-
html += '<h1 style="border:none;font-size:28pt;color:#00D4FF">VIBEDRIFT REPORT</h1>';
|
|
7833
|
-
html += '<p style="font-size:16pt;color:#333">' + esc2(d.project) + '</p>';
|
|
7834
|
-
html += '<p>' + d.fileCount + ' files · ' + d.totalLines.toLocaleString() + ' LOC · Score: <strong>' + d.score + '/' + d.maxScore + '</strong></p>';
|
|
7835
|
-
html += '</div>';
|
|
7836
|
-
|
|
7837
|
-
// Drift findings
|
|
8075
|
+
function buildDocxFindingsHtml(d) {
|
|
8076
|
+
let html = '';
|
|
7838
8077
|
html += '<h1>Drift Findings</h1>';
|
|
7839
8078
|
for (const f of d.driftFindings || []) {
|
|
7840
8079
|
const sev = f.severity === 'error' ? 'critical' : f.severity;
|
|
@@ -7844,8 +8083,6 @@ function exportDOCX() {
|
|
|
7844
8083
|
html += '<div class="drift"><strong>DEVIATING:</strong> ' + esc2(f.devFiles) + '</div>';
|
|
7845
8084
|
if (f.recommendation) html += '<div class="rec"><strong>Fix:</strong> ' + esc2(f.recommendation) + '</div>';
|
|
7846
8085
|
}
|
|
7847
|
-
|
|
7848
|
-
// All findings
|
|
7849
8086
|
html += '<div class="page-break"></div><h1>All Findings</h1>';
|
|
7850
8087
|
html += '<table><tr><th>Severity</th><th>Analyzer</th><th>Finding</th><th>File</th><th>Line</th></tr>';
|
|
7851
8088
|
for (const f of d.findings || []) {
|
|
@@ -7854,16 +8091,17 @@ function exportDOCX() {
|
|
|
7854
8091
|
html += '<td><code>' + esc2(f.file) + '</code></td><td>' + f.line + '</td></tr>';
|
|
7855
8092
|
}
|
|
7856
8093
|
html += '</table>';
|
|
8094
|
+
return html;
|
|
8095
|
+
}
|
|
7857
8096
|
|
|
7858
|
-
|
|
8097
|
+
function buildDocxScoresHtml(d) {
|
|
8098
|
+
let html = '';
|
|
7859
8099
|
html += '<div class="page-break"></div><h1>File Scores</h1>';
|
|
7860
8100
|
html += '<table><tr><th>File</th><th>Score</th><th>Findings</th></tr>';
|
|
7861
8101
|
for (const f of d.fileScores || []) {
|
|
7862
8102
|
html += '<tr><td><code>' + esc2(f.file) + '</code></td><td>' + f.score + '/100</td><td>' + f.findings + '</td></tr>';
|
|
7863
8103
|
}
|
|
7864
8104
|
html += '</table>';
|
|
7865
|
-
|
|
7866
|
-
// Code DNA
|
|
7867
8105
|
if (d.codeDna) {
|
|
7868
8106
|
html += '<div class="page-break"></div><h1>Code DNA Analysis</h1>';
|
|
7869
8107
|
if (d.codeDna.sequences && d.codeDna.sequences.length > 0) {
|
|
@@ -7877,8 +8115,6 @@ function exportDOCX() {
|
|
|
7877
8115
|
html += '</table>';
|
|
7878
8116
|
}
|
|
7879
8117
|
}
|
|
7880
|
-
|
|
7881
|
-
// Deep insights
|
|
7882
8118
|
if (d.deepInsights && d.deepInsights.length > 0) {
|
|
7883
8119
|
html += '<div class="page-break"></div><h1>Deep Analysis Insights (AI-Powered)</h1>';
|
|
7884
8120
|
for (const ins of d.deepInsights) {
|
|
@@ -7887,6 +8123,37 @@ function exportDOCX() {
|
|
|
7887
8123
|
if (ins.recommendation) html += '<div class="rec"><strong>Fix:</strong> ' + esc2(ins.recommendation) + '</div>';
|
|
7888
8124
|
}
|
|
7889
8125
|
}
|
|
8126
|
+
return html;
|
|
8127
|
+
}
|
|
8128
|
+
|
|
8129
|
+
function exportDOCX() {
|
|
8130
|
+
const d = window.__VIBEDRIFT_DATA;
|
|
8131
|
+
if (!d) { alert('Report data not available'); return; }
|
|
8132
|
+
|
|
8133
|
+
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>';
|
|
8134
|
+
html += 'body{font-family:Calibri,sans-serif;font-size:11pt;color:#222;margin:40px}';
|
|
8135
|
+
html += 'h1{font-size:20pt;color:#0B0F15;border-bottom:2px solid #00D4FF;padding-bottom:8px;margin-top:28px}';
|
|
8136
|
+
html += 'h2{font-size:14pt;color:#333;margin-top:20px}';
|
|
8137
|
+
html += 'h3{font-size:12pt;color:#555;margin-top:14px}';
|
|
8138
|
+
html += 'table{border-collapse:collapse;width:100%;margin:10px 0}';
|
|
8139
|
+
html += 'th,td{border:1px solid #ddd;padding:6px 10px;text-align:left;font-size:10pt}';
|
|
8140
|
+
html += 'th{background:#f0f4f8;font-weight:600}';
|
|
8141
|
+
html += '.sev-critical{color:#dc2626;font-weight:700} .sev-warning{color:#d97706;font-weight:700} .sev-info{color:#2563eb}';
|
|
8142
|
+
html += '.intent{background:#ecfdf5;border-left:3px solid #10b981;padding:8px 12px;margin:6px 0}';
|
|
8143
|
+
html += '.drift{background:#fff7ed;border-left:3px solid #f97316;padding:8px 12px;margin:6px 0}';
|
|
8144
|
+
html += '.rec{background:#f0f9ff;border-left:3px solid #0ea5e9;padding:8px 12px;margin:6px 0}';
|
|
8145
|
+
html += 'code{font-family:Consolas,monospace;font-size:9pt;background:#f5f5f5;padding:1px 4px}';
|
|
8146
|
+
html += '.page-break{page-break-before:always}';
|
|
8147
|
+
html += '</style></head><body>';
|
|
8148
|
+
|
|
8149
|
+
html += '<div style="text-align:center;margin-bottom:30px">';
|
|
8150
|
+
html += '<h1 style="border:none;font-size:28pt;color:#00D4FF">VIBEDRIFT REPORT</h1>';
|
|
8151
|
+
html += '<p style="font-size:16pt;color:#333">' + esc2(d.project) + '</p>';
|
|
8152
|
+
html += '<p>' + d.fileCount + ' files · ' + d.totalLines.toLocaleString() + ' LOC · Score: <strong>' + d.score + '/' + d.maxScore + '</strong></p>';
|
|
8153
|
+
html += '</div>';
|
|
8154
|
+
|
|
8155
|
+
html += buildDocxFindingsHtml(d);
|
|
8156
|
+
html += buildDocxScoresHtml(d);
|
|
7890
8157
|
|
|
7891
8158
|
html += '<hr><p style="color:#999;font-size:9pt">Generated by VibeDrift v' + d.version + ' | ' + d.fileCount + ' files | No data sent externally</p>';
|
|
7892
8159
|
html += '</body></html>';
|
|
@@ -8305,9 +8572,8 @@ async function resolveAuthAndBanner(options) {
|
|
|
8305
8572
|
}
|
|
8306
8573
|
return { bearerToken, apiUrl };
|
|
8307
8574
|
}
|
|
8308
|
-
async function
|
|
8575
|
+
async function discoverAndFilterFiles(rootDir, options, spinner) {
|
|
8309
8576
|
const isTerminal = options.format === "terminal" && !options.json;
|
|
8310
|
-
const timings = {};
|
|
8311
8577
|
const t0 = Date.now();
|
|
8312
8578
|
const { ctx, warnings } = await buildAnalysisContext(rootDir);
|
|
8313
8579
|
const includes = options.include ?? [];
|
|
@@ -8321,7 +8587,7 @@ async function runAnalysisPipeline(rootDir, options, spinner) {
|
|
|
8321
8587
|
console.error(chalk2.dim(`[filter] ${before} \u2192 ${filtered.length} files after include/exclude`));
|
|
8322
8588
|
}
|
|
8323
8589
|
}
|
|
8324
|
-
|
|
8590
|
+
const discoveryMs = Date.now() - t0;
|
|
8325
8591
|
if (isTerminal) {
|
|
8326
8592
|
if (warnings.truncated) {
|
|
8327
8593
|
console.warn(chalk2.yellow(`
|
|
@@ -8339,6 +8605,13 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
|
|
|
8339
8605
|
console.log("No source files found to analyze.");
|
|
8340
8606
|
process.exit(0);
|
|
8341
8607
|
}
|
|
8608
|
+
return { ctx, discoveryMs };
|
|
8609
|
+
}
|
|
8610
|
+
async function runAnalysisPipeline(rootDir, options, spinner) {
|
|
8611
|
+
const isTerminal = options.format === "terminal" && !options.json;
|
|
8612
|
+
const timings = {};
|
|
8613
|
+
const { ctx, discoveryMs } = await discoverAndFilterFiles(rootDir, options, spinner);
|
|
8614
|
+
timings.discovery = discoveryMs;
|
|
8342
8615
|
if (spinner) spinner.text = `Parsing ${ctx.files.length} files...`;
|
|
8343
8616
|
const t1 = Date.now();
|
|
8344
8617
|
await parseFiles(ctx.files);
|
|
@@ -8521,6 +8794,12 @@ async function logAndRender(result, options, bearerToken, apiUrl, rootDir, codeD
|
|
|
8521
8794
|
}
|
|
8522
8795
|
}
|
|
8523
8796
|
const format = options.format ?? (options.json ? "json" : "html");
|
|
8797
|
+
await renderToFormat(result, format, options);
|
|
8798
|
+
if (options.failOnScore !== void 0 && compositeScore < options.failOnScore) {
|
|
8799
|
+
process.exit(1);
|
|
8800
|
+
}
|
|
8801
|
+
}
|
|
8802
|
+
async function renderToFormat(result, format, options) {
|
|
8524
8803
|
if (format === "html") {
|
|
8525
8804
|
const html = renderHtmlReport(result);
|
|
8526
8805
|
const outputPath = options.output ?? "vibedrift-report.html";
|
|
@@ -8569,9 +8848,6 @@ async function logAndRender(result, options, bearerToken, apiUrl, rootDir, codeD
|
|
|
8569
8848
|
} else {
|
|
8570
8849
|
console.log(renderTerminalOutput(result));
|
|
8571
8850
|
}
|
|
8572
|
-
if (options.failOnScore !== void 0 && compositeScore < options.failOnScore) {
|
|
8573
|
-
process.exit(1);
|
|
8574
|
-
}
|
|
8575
8851
|
}
|
|
8576
8852
|
async function runScan(targetPath, options) {
|
|
8577
8853
|
const rootDir = resolve(targetPath);
|
|
@@ -8786,58 +9062,7 @@ async function runLogin(options = {}) {
|
|
|
8786
9062
|
}
|
|
8787
9063
|
if (result.status === "pending") continue;
|
|
8788
9064
|
if (result.status === "authorized") {
|
|
8789
|
-
await
|
|
8790
|
-
token: result.access_token,
|
|
8791
|
-
email: result.email,
|
|
8792
|
-
plan: result.plan,
|
|
8793
|
-
expiresAt: result.expires_at,
|
|
8794
|
-
loggedInAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8795
|
-
apiUrl: options.apiUrl
|
|
8796
|
-
});
|
|
8797
|
-
console.log(chalk4.green(" \u2713 Logged in successfully."));
|
|
8798
|
-
console.log("");
|
|
8799
|
-
console.log(` Account: ${chalk4.bold(result.email)}`);
|
|
8800
|
-
console.log(` Plan: ${chalk4.bold(result.plan)}`);
|
|
8801
|
-
console.log("");
|
|
8802
|
-
try {
|
|
8803
|
-
const credits = await fetchCredits(result.access_token, {
|
|
8804
|
-
apiUrl: options.apiUrl
|
|
8805
|
-
});
|
|
8806
|
-
if (credits.has_free_deep_scan && !credits.unlimited) {
|
|
8807
|
-
console.log(
|
|
8808
|
-
chalk4.bgYellow.black.bold(" \u{1F381} 1 FREE deep scan included with your account ")
|
|
8809
|
-
);
|
|
8810
|
-
console.log("");
|
|
8811
|
-
console.log(
|
|
8812
|
-
chalk4.yellow(" Try the full pipeline (Claude analysis, security review,")
|
|
8813
|
-
);
|
|
8814
|
-
console.log(
|
|
8815
|
-
chalk4.yellow(" AI-powered drift detection) on any project \u2014 no card needed.")
|
|
8816
|
-
);
|
|
8817
|
-
console.log("");
|
|
8818
|
-
console.log(` ${chalk4.cyan("vibedrift . --deep")}`);
|
|
8819
|
-
console.log("");
|
|
8820
|
-
} else if (credits.unlimited) {
|
|
8821
|
-
console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
|
|
8822
|
-
console.log("");
|
|
8823
|
-
} else if (credits.available_total > 0) {
|
|
8824
|
-
console.log(
|
|
8825
|
-
chalk4.dim(` You have ${credits.available_total} deep scan credit${credits.available_total === 1 ? "" : "s"} available.`)
|
|
8826
|
-
);
|
|
8827
|
-
console.log(chalk4.dim(" Run `vibedrift . --deep` to use one."));
|
|
8828
|
-
console.log("");
|
|
8829
|
-
} else {
|
|
8830
|
-
console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
|
|
8831
|
-
console.log("");
|
|
8832
|
-
}
|
|
8833
|
-
} catch {
|
|
8834
|
-
if (result.plan === "free") {
|
|
8835
|
-
console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
|
|
8836
|
-
} else {
|
|
8837
|
-
console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
|
|
8838
|
-
}
|
|
8839
|
-
console.log("");
|
|
8840
|
-
}
|
|
9065
|
+
await handleLoginSuccess(result, options);
|
|
8841
9066
|
return;
|
|
8842
9067
|
}
|
|
8843
9068
|
if (result.status === "denied") {
|
|
@@ -8855,6 +9080,60 @@ async function runLogin(options = {}) {
|
|
|
8855
9080
|
console.error(chalk4.dim(" Run `vibedrift login` again to retry.\n"));
|
|
8856
9081
|
process.exit(1);
|
|
8857
9082
|
}
|
|
9083
|
+
async function handleLoginSuccess(result, options) {
|
|
9084
|
+
await patchConfig({
|
|
9085
|
+
token: result.access_token,
|
|
9086
|
+
email: result.email,
|
|
9087
|
+
plan: result.plan,
|
|
9088
|
+
expiresAt: result.expires_at,
|
|
9089
|
+
loggedInAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9090
|
+
apiUrl: options.apiUrl
|
|
9091
|
+
});
|
|
9092
|
+
console.log(chalk4.green(" \u2713 Logged in successfully."));
|
|
9093
|
+
console.log("");
|
|
9094
|
+
console.log(` Account: ${chalk4.bold(result.email)}`);
|
|
9095
|
+
console.log(` Plan: ${chalk4.bold(result.plan)}`);
|
|
9096
|
+
console.log("");
|
|
9097
|
+
try {
|
|
9098
|
+
const credits = await fetchCredits(result.access_token, {
|
|
9099
|
+
apiUrl: options.apiUrl
|
|
9100
|
+
});
|
|
9101
|
+
if (credits.has_free_deep_scan && !credits.unlimited) {
|
|
9102
|
+
console.log(
|
|
9103
|
+
chalk4.bgYellow.black.bold(" \u{1F381} 1 FREE deep scan included with your account ")
|
|
9104
|
+
);
|
|
9105
|
+
console.log("");
|
|
9106
|
+
console.log(
|
|
9107
|
+
chalk4.yellow(" Try the full pipeline (Claude analysis, security review,")
|
|
9108
|
+
);
|
|
9109
|
+
console.log(
|
|
9110
|
+
chalk4.yellow(" AI-powered drift detection) on any project \u2014 no card needed.")
|
|
9111
|
+
);
|
|
9112
|
+
console.log("");
|
|
9113
|
+
console.log(` ${chalk4.cyan("vibedrift . --deep")}`);
|
|
9114
|
+
console.log("");
|
|
9115
|
+
} else if (credits.unlimited) {
|
|
9116
|
+
console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
|
|
9117
|
+
console.log("");
|
|
9118
|
+
} else if (credits.available_total > 0) {
|
|
9119
|
+
console.log(
|
|
9120
|
+
chalk4.dim(` You have ${credits.available_total} deep scan credit${credits.available_total === 1 ? "" : "s"} available.`)
|
|
9121
|
+
);
|
|
9122
|
+
console.log(chalk4.dim(" Run `vibedrift . --deep` to use one."));
|
|
9123
|
+
console.log("");
|
|
9124
|
+
} else {
|
|
9125
|
+
console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
|
|
9126
|
+
console.log("");
|
|
9127
|
+
}
|
|
9128
|
+
} catch {
|
|
9129
|
+
if (result.plan === "free") {
|
|
9130
|
+
console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
|
|
9131
|
+
} else {
|
|
9132
|
+
console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
|
|
9133
|
+
}
|
|
9134
|
+
console.log("");
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
8858
9137
|
function fail(intro, err) {
|
|
8859
9138
|
const msg = err instanceof VibeDriftApiError ? `${err.status ? `HTTP ${err.status}: ` : ""}${err.message}` : err instanceof Error ? err.message : String(err);
|
|
8860
9139
|
console.error(chalk4.red(`
|
|
@@ -9102,58 +9381,8 @@ import { homedir as homedir3, platform, arch } from "os";
|
|
|
9102
9381
|
import { join as join7 } from "path";
|
|
9103
9382
|
import { stat as stat4, access, constants } from "fs/promises";
|
|
9104
9383
|
init_version();
|
|
9105
|
-
async function
|
|
9106
|
-
let
|
|
9107
|
-
console.log("");
|
|
9108
|
-
console.log(chalk10.bold(" VibeDrift Doctor"));
|
|
9109
|
-
console.log("");
|
|
9110
|
-
console.log(chalk10.bold(" Environment"));
|
|
9111
|
-
ok("CLI version", getVersion());
|
|
9112
|
-
ok("Node", process.version);
|
|
9113
|
-
ok("Platform", `${platform()} ${arch()}`);
|
|
9114
|
-
ok("HOME", homedir3());
|
|
9115
|
-
console.log("");
|
|
9116
|
-
console.log(chalk10.bold(" Config"));
|
|
9117
|
-
const configDir = getConfigDir();
|
|
9118
|
-
const configPath = getConfigPath();
|
|
9119
|
-
let configDirOk = false;
|
|
9120
|
-
try {
|
|
9121
|
-
const info2 = await stat4(configDir);
|
|
9122
|
-
if (info2.isDirectory()) {
|
|
9123
|
-
configDirOk = true;
|
|
9124
|
-
const mode = (info2.mode & 511).toString(8);
|
|
9125
|
-
ok("Config dir", `${configDir} (mode ${mode})`);
|
|
9126
|
-
} else {
|
|
9127
|
-
bad(`Config dir exists but is not a directory: ${configDir}`);
|
|
9128
|
-
failures++;
|
|
9129
|
-
}
|
|
9130
|
-
} catch {
|
|
9131
|
-
info("Config dir", `${configDir} (will be created on first login)`);
|
|
9132
|
-
configDirOk = true;
|
|
9133
|
-
}
|
|
9134
|
-
if (configDirOk) {
|
|
9135
|
-
try {
|
|
9136
|
-
await access(configPath, constants.R_OK);
|
|
9137
|
-
const info2 = await stat4(configPath);
|
|
9138
|
-
const mode = (info2.mode & 511).toString(8);
|
|
9139
|
-
if ((info2.mode & 63) !== 0) {
|
|
9140
|
-
warn("Config file", `${configPath} (mode ${mode}, world/group readable \u2014 should be 600)`);
|
|
9141
|
-
} else {
|
|
9142
|
-
ok("Config file", `${configPath} (mode ${mode})`);
|
|
9143
|
-
}
|
|
9144
|
-
} catch {
|
|
9145
|
-
info("Config file", "absent (not logged in)");
|
|
9146
|
-
}
|
|
9147
|
-
}
|
|
9148
|
-
const historyDir = join7(homedir3(), ".vibedrift", "scans");
|
|
9149
|
-
try {
|
|
9150
|
-
const info2 = await stat4(historyDir);
|
|
9151
|
-
if (info2.isDirectory()) ok("Scan history", historyDir);
|
|
9152
|
-
else warn("Scan history", `${historyDir} exists but is not a directory`);
|
|
9153
|
-
} catch {
|
|
9154
|
-
info("Scan history", "empty (no scans run yet)");
|
|
9155
|
-
}
|
|
9156
|
-
console.log("");
|
|
9384
|
+
async function checkAuthStatus() {
|
|
9385
|
+
let authFailures = 0;
|
|
9157
9386
|
console.log(chalk10.bold(" Authentication"));
|
|
9158
9387
|
const config = await readConfig();
|
|
9159
9388
|
const resolved = await resolveToken();
|
|
@@ -9170,7 +9399,7 @@ async function runDoctor() {
|
|
|
9170
9399
|
const now = Date.now();
|
|
9171
9400
|
if (expires < now) {
|
|
9172
9401
|
bad(`Token expired ${Math.floor((now - expires) / 864e5)} days ago`);
|
|
9173
|
-
|
|
9402
|
+
authFailures++;
|
|
9174
9403
|
} else {
|
|
9175
9404
|
ok("Token expires", `${config.expiresAt} (${Math.ceil((expires - now) / 864e5)} days)`);
|
|
9176
9405
|
}
|
|
@@ -9178,6 +9407,10 @@ async function runDoctor() {
|
|
|
9178
9407
|
}
|
|
9179
9408
|
}
|
|
9180
9409
|
console.log("");
|
|
9410
|
+
return { resolved, authFailures };
|
|
9411
|
+
}
|
|
9412
|
+
async function checkApiConnectivity(resolved) {
|
|
9413
|
+
let failures = 0;
|
|
9181
9414
|
console.log(chalk10.bold(" API"));
|
|
9182
9415
|
const apiUrl = await resolveApiUrl();
|
|
9183
9416
|
ok("API URL", apiUrl);
|
|
@@ -9212,6 +9445,64 @@ async function runDoctor() {
|
|
|
9212
9445
|
}
|
|
9213
9446
|
}
|
|
9214
9447
|
console.log("");
|
|
9448
|
+
return failures;
|
|
9449
|
+
}
|
|
9450
|
+
async function runDoctor() {
|
|
9451
|
+
let failures = 0;
|
|
9452
|
+
console.log("");
|
|
9453
|
+
console.log(chalk10.bold(" VibeDrift Doctor"));
|
|
9454
|
+
console.log("");
|
|
9455
|
+
console.log(chalk10.bold(" Environment"));
|
|
9456
|
+
ok("CLI version", getVersion());
|
|
9457
|
+
ok("Node", process.version);
|
|
9458
|
+
ok("Platform", `${platform()} ${arch()}`);
|
|
9459
|
+
ok("HOME", homedir3());
|
|
9460
|
+
console.log("");
|
|
9461
|
+
console.log(chalk10.bold(" Config"));
|
|
9462
|
+
const configDir = getConfigDir();
|
|
9463
|
+
const configPath = getConfigPath();
|
|
9464
|
+
let configDirOk = false;
|
|
9465
|
+
try {
|
|
9466
|
+
const info2 = await stat4(configDir);
|
|
9467
|
+
if (info2.isDirectory()) {
|
|
9468
|
+
configDirOk = true;
|
|
9469
|
+
const mode = (info2.mode & 511).toString(8);
|
|
9470
|
+
ok("Config dir", `${configDir} (mode ${mode})`);
|
|
9471
|
+
} else {
|
|
9472
|
+
bad(`Config dir exists but is not a directory: ${configDir}`);
|
|
9473
|
+
failures++;
|
|
9474
|
+
}
|
|
9475
|
+
} catch {
|
|
9476
|
+
info("Config dir", `${configDir} (will be created on first login)`);
|
|
9477
|
+
configDirOk = true;
|
|
9478
|
+
}
|
|
9479
|
+
if (configDirOk) {
|
|
9480
|
+
try {
|
|
9481
|
+
await access(configPath, constants.R_OK);
|
|
9482
|
+
const info2 = await stat4(configPath);
|
|
9483
|
+
const mode = (info2.mode & 511).toString(8);
|
|
9484
|
+
if ((info2.mode & 63) !== 0) {
|
|
9485
|
+
warn("Config file", `${configPath} (mode ${mode}, world/group readable \u2014 should be 600)`);
|
|
9486
|
+
} else {
|
|
9487
|
+
ok("Config file", `${configPath} (mode ${mode})`);
|
|
9488
|
+
}
|
|
9489
|
+
} catch {
|
|
9490
|
+
info("Config file", "absent (not logged in)");
|
|
9491
|
+
}
|
|
9492
|
+
}
|
|
9493
|
+
const historyDir = join7(homedir3(), ".vibedrift", "scans");
|
|
9494
|
+
try {
|
|
9495
|
+
const info2 = await stat4(historyDir);
|
|
9496
|
+
if (info2.isDirectory()) ok("Scan history", historyDir);
|
|
9497
|
+
else warn("Scan history", `${historyDir} exists but is not a directory`);
|
|
9498
|
+
} catch {
|
|
9499
|
+
info("Scan history", "empty (no scans run yet)");
|
|
9500
|
+
}
|
|
9501
|
+
console.log("");
|
|
9502
|
+
const { resolved, authFailures } = await checkAuthStatus();
|
|
9503
|
+
failures += authFailures;
|
|
9504
|
+
const apiFailures = await checkApiConnectivity(resolved);
|
|
9505
|
+
failures += apiFailures;
|
|
9215
9506
|
if (failures === 0) {
|
|
9216
9507
|
console.log(chalk10.green(" \u2713 All checks passed."));
|
|
9217
9508
|
} else {
|