@vibedrift/cli 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -598,63 +598,72 @@ function extractAssignedVariable(line) {
598
598
  if (pyMatch) return pyMatch[1];
599
599
  return null;
600
600
  }
601
- function analyzeFunction(fn) {
602
- const flows = [];
603
- const lines = fn.rawBody.split("\n");
604
- const taintedVars = /* @__PURE__ */ new Map();
605
- const langSources = TAINT_SOURCES[fn.language] ?? [];
606
- for (let i = 0; i < lines.length; i++) {
607
- const line = lines[i];
608
- const trimmed = line.trim();
609
- for (const src of langSources) {
610
- if (src.regex.test(trimmed)) {
611
- const varName = extractAssignedVariable(trimmed);
612
- if (varName) {
613
- taintedVars.set(varName, {
614
- name: varName,
615
- source: { type: src.label, variable: varName, line: fn.line + i },
616
- sanitizedFor: /* @__PURE__ */ new Set()
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
- if (san.removes === "all") {
626
- tainted.sanitizedFor = /* @__PURE__ */ new Set(["sql_injection", "command_injection", "path_traversal", "xss", "ssrf", "code_injection"]);
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 lines = file.content.split("\n");
828
- const signals = [];
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 sanitizeResultForUpload(result) {
1668
- const ctx = result.context;
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
- const obj = {};
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
- const envelope = {
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 renderCsvReport(result) {
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 cs = result.scores;
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
- if ((result.driftFindings ?? []).length > 0) {
1791
- lines.push("DRIFT FINDINGS");
1792
- lines.push(row("Severity", "Category", "Finding", "Dominant Pattern", "Dominant Count", "Total Files", "Consistency %", "Deviating Files", "Recommendation"));
1793
- for (const d of result.driftFindings) {
1794
- const devFiles = d.deviatingFiles.map((f) => f.path).join("; ");
1795
- lines.push(row(
1796
- d.severity,
1797
- d.driftCategory,
1798
- d.finding,
1799
- d.dominantPattern,
1800
- d.dominantCount,
1801
- d.totalRelevantFiles,
1802
- d.consistencyScore,
1803
- devFiles,
1804
- d.recommendation
1805
- ));
1806
- }
1807
- lines.push("");
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
- const dna = result.codeDnaResult;
1810
- if (dna) {
1811
- if (dna.duplicateGroups?.length > 0) {
1812
- lines.push("CODE DNA: SEMANTIC DUPLICATES");
1813
- lines.push(row("Group", "Functions", "Files"));
1814
- for (const g of dna.duplicateGroups) {
1815
- const fns = g.functions.map((f) => f.name + "()").join("; ");
1816
- const files = g.functions.map((f) => f.relativePath || f.file).join("; ");
1817
- lines.push(row(g.groupId, fns, files));
1818
- }
1819
- lines.push("");
1820
- }
1821
- if (dna.sequenceSimilarities?.length > 0) {
1822
- lines.push("CODE DNA: OPERATION SEQUENCE MATCHES");
1823
- lines.push(row("Function A", "File A", "Function B", "File B", "Similarity %"));
1824
- for (const s of dna.sequenceSimilarities) {
1825
- lines.push(row(
1826
- s.functionA.name,
1827
- s.functionA.relativePath || s.functionA.file,
1828
- s.functionB.name,
1829
- s.functionB.relativePath || s.functionB.file,
1830
- Math.round(s.similarity * 100)
1831
- ));
1832
- }
1833
- lines.push("");
1834
- }
1835
- if (dna.taintFlows?.length > 0) {
1836
- lines.push("CODE DNA: TAINT FLOWS");
1837
- lines.push(row("File", "Function", "Source Type", "Source Line", "Sink Type", "Sink Line", "Sanitized"));
1838
- for (const t of dna.taintFlows) {
1839
- lines.push(row(
1840
- t.relativePath || t.file,
1841
- t.functionName,
1842
- t.source.type,
1843
- t.source.line,
1844
- t.sink.type,
1845
- t.sink.line,
1846
- t.sanitized ? "Yes" : "No"
1847
- ));
1848
- }
1849
- lines.push("");
1850
- }
1851
- if (dna.deviationJustifications?.length > 0) {
1852
- lines.push("CODE DNA: DEVIATION ANALYSIS");
1853
- lines.push(row("File", "Deviating Pattern", "Dominant Pattern", "Verdict", "Score"));
1854
- for (const dj of dna.deviationJustifications) {
1855
- lines.push(row(
1856
- dj.relativePath || dj.file,
1857
- dj.deviatingPattern,
1858
- dj.dominantPattern,
1859
- dj.verdict,
1860
- Math.round(dj.justificationScore * 100)
1861
- ));
1862
- }
1863
- lines.push("");
1864
- }
1865
- if (dna.patternDistributions?.length > 0) {
1866
- lines.push("CODE DNA: PATTERN DISTRIBUTIONS");
1867
- lines.push(row("File", "Dominant Pattern", "Confidence", "Internally Inconsistent"));
1868
- for (const pd of dna.patternDistributions) {
1869
- lines.push(row(
1870
- pd.relativePath || pd.file,
1871
- pd.dominantPattern,
1872
- Math.round(pd.confidence * 100),
1873
- pd.isInternallyInconsistent ? "Yes" : "No"
1874
- ));
1875
- }
1876
- lines.push("");
1877
- }
1845
+ lines.push("");
1846
+ return lines;
1847
+ }
1848
+ function csvDuplicateGroups(dna) {
1849
+ if (!dna.duplicateGroups?.length) return [];
1850
+ const lines = [];
1851
+ lines.push("CODE DNA: SEMANTIC DUPLICATES");
1852
+ lines.push(row("Group", "Functions", "Files"));
1853
+ for (const g of dna.duplicateGroups) {
1854
+ const fns = g.functions.map((f) => f.name + "()").join("; ");
1855
+ const files = g.functions.map((f) => f.relativePath || f.file).join("; ");
1856
+ lines.push(row(g.groupId, fns, files));
1857
+ }
1858
+ lines.push("");
1859
+ return lines;
1860
+ }
1861
+ function csvSequenceSimilarities(dna) {
1862
+ if (!dna.sequenceSimilarities?.length) return [];
1863
+ const lines = [];
1864
+ lines.push("CODE DNA: OPERATION SEQUENCE MATCHES");
1865
+ lines.push(row("Function A", "File A", "Function B", "File B", "Similarity %"));
1866
+ for (const s of dna.sequenceSimilarities) {
1867
+ lines.push(row(
1868
+ s.functionA.name,
1869
+ s.functionA.relativePath || s.functionA.file,
1870
+ s.functionB.name,
1871
+ s.functionB.relativePath || s.functionB.file,
1872
+ Math.round(s.similarity * 100)
1873
+ ));
1874
+ }
1875
+ lines.push("");
1876
+ return lines;
1877
+ }
1878
+ function csvTaintFlows(dna) {
1879
+ if (!dna.taintFlows?.length) return [];
1880
+ const lines = [];
1881
+ lines.push("CODE DNA: TAINT FLOWS");
1882
+ lines.push(row("File", "Function", "Source Type", "Source Line", "Sink Type", "Sink Line", "Sanitized"));
1883
+ for (const t of dna.taintFlows) {
1884
+ lines.push(row(
1885
+ t.relativePath || t.file,
1886
+ t.functionName,
1887
+ t.source.type,
1888
+ t.source.line,
1889
+ t.sink.type,
1890
+ t.sink.line,
1891
+ t.sanitized ? "Yes" : "No"
1892
+ ));
1893
+ }
1894
+ lines.push("");
1895
+ return lines;
1896
+ }
1897
+ function csvDeviations(dna) {
1898
+ if (!dna.deviationJustifications?.length) return [];
1899
+ const lines = [];
1900
+ lines.push("CODE DNA: DEVIATION ANALYSIS");
1901
+ lines.push(row("File", "Deviating Pattern", "Dominant Pattern", "Verdict", "Score"));
1902
+ for (const dj of dna.deviationJustifications) {
1903
+ lines.push(row(
1904
+ dj.relativePath || dj.file,
1905
+ dj.deviatingPattern,
1906
+ dj.dominantPattern,
1907
+ dj.verdict,
1908
+ Math.round(dj.justificationScore * 100)
1909
+ ));
1878
1910
  }
1911
+ lines.push("");
1912
+ return lines;
1913
+ }
1914
+ function csvPatterns(dna) {
1915
+ if (!dna.patternDistributions?.length) return [];
1916
+ const lines = [];
1917
+ lines.push("CODE DNA: PATTERN DISTRIBUTIONS");
1918
+ lines.push(row("File", "Dominant Pattern", "Confidence", "Internally Inconsistent"));
1919
+ for (const pd of dna.patternDistributions) {
1920
+ lines.push(row(
1921
+ pd.relativePath || pd.file,
1922
+ pd.dominantPattern,
1923
+ Math.round(pd.confidence * 100),
1924
+ pd.isInternallyInconsistent ? "Yes" : "No"
1925
+ ));
1926
+ }
1927
+ lines.push("");
1928
+ return lines;
1929
+ }
1930
+ function csvCodeDna(result) {
1931
+ const dna = result.codeDnaResult;
1932
+ if (!dna) return [];
1933
+ return [
1934
+ ...csvDuplicateGroups(dna),
1935
+ ...csvSequenceSimilarities(dna),
1936
+ ...csvTaintFlows(dna),
1937
+ ...csvDeviations(dna),
1938
+ ...csvPatterns(dna)
1939
+ ];
1940
+ }
1941
+ function csvFindings(result) {
1942
+ const lines = [];
1879
1943
  lines.push("ALL FINDINGS");
1880
1944
  lines.push(row("Severity", "Analyzer", "Confidence %", "Message", "File", "Line", "Tags"));
1881
1945
  for (const f of result.findings) {
@@ -1891,6 +1955,10 @@ function renderCsvReport(result) {
1891
1955
  ));
1892
1956
  }
1893
1957
  lines.push("");
1958
+ return lines;
1959
+ }
1960
+ function csvPerFileScores(result) {
1961
+ const lines = [];
1894
1962
  lines.push("PER-FILE SCORES");
1895
1963
  lines.push(row("File", "Score", "Finding Count"));
1896
1964
  const fileSorted = [...result.perFileScores.entries()].sort((a, b) => a[1].score - b[1].score);
@@ -1898,21 +1966,35 @@ function renderCsvReport(result) {
1898
1966
  lines.push(row(path2, data.score, data.findings.length));
1899
1967
  }
1900
1968
  lines.push("");
1901
- if ((result.deepInsights ?? []).length > 0) {
1902
- lines.push("DEEP ANALYSIS INSIGHTS");
1903
- lines.push(row("Category", "Severity", "Title", "Description", "Related Files", "Recommendation"));
1904
- for (const ins of result.deepInsights) {
1905
- lines.push(row(
1906
- ins.category,
1907
- ins.severity,
1908
- ins.title,
1909
- ins.description,
1910
- ins.relatedFiles.join("; "),
1911
- ins.recommendation ?? ""
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.join("\n");
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 buildDocumentXml(result) {
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
- parts.push(tableRow([
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
- for (const [catName, catScore] of catRows) {
2168
+ ]);
2169
+ const dataRows = catRows.map(([catName, catScore]) => {
2081
2170
  const cs = catScore;
2082
- parts.push(tableRow([
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
- const scoreTable = `<w:tbl><w:tblPr><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><w:tblW w:w="5000" w:type="pct"/></w:tblPr>${parts.splice(-6).join("")}</w:tbl>`;
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
- const dna = result.codeDnaResult;
2136
- if (dna) {
2137
- parts.push(para("4. CODE DNA ANALYSIS", "Heading1"));
2138
- parts.push(para(`${dna.functions?.length ?? 0} functions analyzed in ${dna.timings?.totalMs ?? 0}ms. ${dna.findings?.length ?? 0} findings.`));
2139
- parts.push(para(""));
2140
- if (dna.duplicateGroups?.length > 0) {
2141
- parts.push(para("Semantic Fingerprint Duplicates", "Heading2"));
2142
- for (const g of dna.duplicateGroups) {
2143
- const fns = g.functions.map((f) => `${f.name}() in ${f.relativePath || f.file}`).join(", ");
2144
- parts.push(para(` Group: ${fns}`));
2145
- }
2146
- parts.push(para(""));
2147
- }
2148
- if (dna.sequenceSimilarities?.length > 0) {
2149
- parts.push(para("Operation Sequence Matches", "Heading2"));
2150
- for (const s of dna.sequenceSimilarities) {
2151
- 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}`));
2152
- }
2153
- parts.push(para(""));
2154
- }
2155
- if (dna.taintFlows?.length > 0) {
2156
- parts.push(para("Taint Flows", "Heading2"));
2157
- for (const t of dna.taintFlows) {
2158
- 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"}`));
2159
- }
2160
- parts.push(para(""));
2161
- }
2162
- if (dna.deviationJustifications?.length > 0) {
2163
- parts.push(para("Deviation Justification Analysis", "Heading2"));
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"}`));
2260
+ }
2261
+ parts.push(para(""));
2262
+ return parts;
2263
+ }
2264
+ function docxDnaDeviations(dna) {
2265
+ if (!dna.deviationJustifications?.length) return [];
2266
+ const parts = [];
2267
+ parts.push(para("Deviation Justification Analysis", "Heading2"));
2268
+ for (const dj of dna.deviationJustifications) {
2269
+ const verdict = dj.verdict === "likely_justified" ? "JUSTIFIED" : dj.verdict === "likely_accidental" ? "ACCIDENTAL" : "UNCERTAIN";
2270
+ parts.push(para(` [${verdict}] ${dj.relativePath || dj.file}: uses ${dj.deviatingPattern} vs project ${dj.dominantPattern} (score: ${Math.round(dj.justificationScore * 100)}%)`));
2271
+ }
2272
+ parts.push(para(""));
2273
+ return parts;
2274
+ }
2275
+ function docxDnaPatterns(dna) {
2276
+ if (!dna.patternDistributions?.length) return [];
2277
+ const parts = [];
2278
+ parts.push(para("Pattern Classification", "Heading2"));
2279
+ for (const pd of dna.patternDistributions) {
2280
+ const mixed = pd.isInternallyInconsistent ? " [MIXED]" : "";
2281
+ parts.push(para(` ${pd.relativePath || pd.file}: ${pd.dominantPattern} (${Math.round(pd.confidence * 100)}% confidence)${mixed}`));
2179
2282
  }
2283
+ parts.push(para(""));
2284
+ return parts;
2285
+ }
2286
+ function buildDocxCodeDnaSection(result) {
2287
+ const dna = result.codeDnaResult;
2288
+ if (!dna) return "";
2289
+ const parts = [];
2290
+ parts.push(para("4. CODE DNA ANALYSIS", "Heading1"));
2291
+ parts.push(para(`${dna.functions?.length ?? 0} functions analyzed in ${dna.timings?.totalMs ?? 0}ms. ${dna.findings?.length ?? 0} findings.`));
2292
+ parts.push(para(""));
2293
+ parts.push(
2294
+ ...docxDnaDuplicates(dna),
2295
+ ...docxDnaSequences(dna),
2296
+ ...docxDnaTaintFlows(dna),
2297
+ ...docxDnaDeviations(dna),
2298
+ ...docxDnaPatterns(dna)
2299
+ );
2300
+ parts.push(hr());
2301
+ return parts.join("\n ");
2302
+ }
2303
+ function buildDocxPerFileTable(result) {
2304
+ const parts = [];
2180
2305
  parts.push(para("5. FILE RANKING", "Heading1"));
2181
2306
  const fileSorted = [...result.perFileScores.entries()].sort((a, b) => a[1].score - b[1].score);
2182
2307
  parts.push(para(`${fileSorted.length} files scanned. Ranked worst to best.`));
@@ -2197,8 +2322,12 @@ function buildDocumentXml(result) {
2197
2322
  tableCellSimple(String(staticCount))
2198
2323
  ]);
2199
2324
  }).join("");
2200
- parts.push(`<w:tbl><w:tblPr><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><w:tblW w:w="5000" w:type="pct"/></w:tblPr>${fileHeaderRow}${fileDataRows}</w:tbl>`);
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
- if ((result.deepInsights ?? []).length > 0) {
2215
- parts.push(para("7. DEEP ANALYSIS INSIGHTS (AI-POWERED)", "Heading1"));
2216
- for (const ins of result.deepInsights) {
2217
- const sevStr = ins.severity === "error" ? "CRITICAL" : ins.severity === "warning" ? "WARNING" : "INFO";
2218
- parts.push(para(`[${sevStr}] ${ins.title}`, "Heading2"));
2219
- parts.push(para(ins.description));
2220
- if (ins.relatedFiles.length > 0) parts.push(para(` Files: ${ins.relatedFiles.join(", ")}`));
2221
- if (ins.recommendation) {
2222
- parts.push(boldPara("Recommendation:", "00D4FF"));
2223
- parts.push(para(` ${ins.recommendation}`));
2224
- }
2225
- parts.push(para(""));
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(hr());
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
- ${parts.join("\n ")}
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
 
@@ -5153,20 +5304,7 @@ function buildPatternDistribution(profiles) {
5153
5304
  }
5154
5305
  return counts;
5155
5306
  }
5156
- function analyzePatternDistribution(profiles, patternNames) {
5157
- const counts = buildPatternDistribution(profiles);
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);
5307
+ function collectDeviatingFiles(counts, dominant, profiles, patternNames) {
5170
5308
  const deviating = [];
5171
5309
  for (const [pattern, data] of counts) {
5172
5310
  if (pattern === dominant) continue;
@@ -5180,6 +5318,29 @@ function analyzePatternDistribution(profiles, patternNames) {
5180
5318
  });
5181
5319
  }
5182
5320
  }
5321
+ return deviating;
5322
+ }
5323
+ function findDominantPattern(counts) {
5324
+ let dominant = null;
5325
+ let dominantCount = 0;
5326
+ for (const [pattern, data] of counts) {
5327
+ if (data.count > dominantCount) {
5328
+ dominantCount = data.count;
5329
+ dominant = pattern;
5330
+ }
5331
+ }
5332
+ if (!dominant) return null;
5333
+ return { dominant, dominantCount };
5334
+ }
5335
+ function analyzePatternDistribution(profiles, patternNames) {
5336
+ const counts = buildPatternDistribution(profiles);
5337
+ if (counts.size < 2) return null;
5338
+ const result = findDominantPattern(counts);
5339
+ if (!result) return null;
5340
+ const { dominant, dominantCount } = result;
5341
+ const totalFiles = profiles.length;
5342
+ const consistencyScore = Math.round(dominantCount / totalFiles * 100);
5343
+ const deviating = collectDeviatingFiles(counts, dominant, profiles, patternNames);
5183
5344
  if (deviating.length === 0) return null;
5184
5345
  return {
5185
5346
  detector: "architectural_consistency",
@@ -5281,13 +5442,25 @@ function classifyName(name) {
5281
5442
  if (/^[a-z][a-z0-9]*$/.test(name)) return "camelCase";
5282
5443
  return null;
5283
5444
  }
5284
- function isIdiomatic(name, convention, symbolType, language) {
5285
- if (language === "go" && convention === "PascalCase" && /^[A-Z]/.test(name)) return true;
5286
- if (language === "go" && /^(?:HTTP|URL|ID|JSON|XML|SQL|API|DNS|TCP|UDP|IP|TLS|SSH|EOF)/.test(name)) return true;
5287
- if ((language === "javascript" || language === "typescript") && symbolType === "class" && convention === "PascalCase") return true;
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;
5445
+ function isIdiomaticGo(name, convention) {
5446
+ if (convention === "PascalCase" && /^[A-Z]/.test(name)) return true;
5447
+ if (/^(?:HTTP|URL|ID|JSON|XML|SQL|API|DNS|TCP|UDP|IP|TLS|SSH|EOF)/.test(name)) return true;
5448
+ return false;
5449
+ }
5450
+ function isIdiomaticJsTs(name, convention, symbolType) {
5451
+ if (symbolType === "class" && convention === "PascalCase") return true;
5452
+ if (symbolType === "function" && convention === "PascalCase" && /^[A-Z]\w*$/.test(name)) return true;
5453
+ return false;
5454
+ }
5455
+ function isIdiomaticPython(name, convention, symbolType) {
5456
+ if (symbolType === "class" && convention === "PascalCase") return true;
5457
+ if (name.startsWith("__") && name.endsWith("__")) return true;
5458
+ return false;
5459
+ }
5460
+ function isIdiomatic(name, convention, symbolType, language) {
5461
+ if (language === "go" && isIdiomaticGo(name, convention)) return true;
5462
+ if ((language === "javascript" || language === "typescript") && isIdiomaticJsTs(name, convention, symbolType)) return true;
5463
+ if (language === "python" && isIdiomaticPython(name, convention, symbolType)) return true;
5291
5464
  if (symbolType === "constant" && convention === "SCREAMING_SNAKE") return true;
5292
5465
  return false;
5293
5466
  }
@@ -5382,6 +5555,68 @@ function analyzeFileNaming(files) {
5382
5555
  recommendation: `Standardize file names to ${dominant}. ${deviating.length} files use a different convention.`
5383
5556
  };
5384
5557
  }
5558
+ function collectDeviantFiles(convCounts, dominant) {
5559
+ const fileDeviants = /* @__PURE__ */ new Map();
5560
+ for (const [conv, syms] of convCounts) {
5561
+ if (conv === dominant) continue;
5562
+ for (const s of syms) {
5563
+ if (!fileDeviants.has(s.file)) fileDeviants.set(s.file, []);
5564
+ fileDeviants.get(s.file).push(s);
5565
+ }
5566
+ }
5567
+ const deviatingFiles = [];
5568
+ for (const [filePath, syms] of fileDeviants) {
5569
+ const uniqueConventions = [...new Set(syms.map((s) => s.convention))];
5570
+ deviatingFiles.push({
5571
+ path: filePath,
5572
+ detectedPattern: uniqueConventions.join(", "),
5573
+ evidence: syms.slice(0, 3).map((s) => ({ line: s.line, code: s.name }))
5574
+ });
5575
+ }
5576
+ return deviatingFiles;
5577
+ }
5578
+ function buildConventionFinding(type, dominant, maxCount, totalSymbols, deviatingFiles) {
5579
+ const deviantCount = totalSymbols - maxCount;
5580
+ const consistencyScore = Math.round(maxCount / totalSymbols * 100);
5581
+ return {
5582
+ detector: "naming_conventions",
5583
+ subCategory: `${type}_names`,
5584
+ driftCategory: "naming_conventions",
5585
+ severity: deviatingFiles.length > 5 ? "error" : "warning",
5586
+ confidence: 0.8,
5587
+ finding: `${type} naming convention oscillates: ${maxCount} use ${dominant}, ${deviantCount} use other conventions \u2014 likely from different AI sessions`,
5588
+ dominantPattern: dominant,
5589
+ dominantCount: maxCount,
5590
+ totalRelevantFiles: totalSymbols,
5591
+ consistencyScore,
5592
+ deviatingFiles: deviatingFiles.slice(0, 10),
5593
+ recommendation: `${maxCount} of ${totalSymbols} ${type} names use ${dominant}. Standardize deviating names.`
5594
+ };
5595
+ }
5596
+ function analyzeSymbolTypeConventions(type, typeSymbols) {
5597
+ if (typeSymbols.length < 3) return null;
5598
+ const convCounts = /* @__PURE__ */ new Map();
5599
+ for (const s of typeSymbols) {
5600
+ if (!convCounts.has(s.convention)) convCounts.set(s.convention, []);
5601
+ convCounts.get(s.convention).push(s);
5602
+ }
5603
+ if (convCounts.size < 2) return null;
5604
+ let dominant = null;
5605
+ let maxCount = 0;
5606
+ for (const [conv, syms] of convCounts) {
5607
+ if (syms.length > maxCount) {
5608
+ maxCount = syms.length;
5609
+ dominant = conv;
5610
+ }
5611
+ }
5612
+ if (!dominant) return null;
5613
+ const totalSymbols = typeSymbols.length;
5614
+ const deviantCount = totalSymbols - maxCount;
5615
+ if (deviantCount < 3 || deviantCount / totalSymbols < 0.1) return null;
5616
+ const deviatingFiles = collectDeviantFiles(convCounts, dominant);
5617
+ if (deviatingFiles.length < 2) return null;
5618
+ return buildConventionFinding(type, dominant, maxCount, totalSymbols, deviatingFiles);
5619
+ }
5385
5620
  var conventionOscillation = {
5386
5621
  id: "convention-oscillation",
5387
5622
  name: "Naming Convention Oscillation",
@@ -5396,62 +5631,8 @@ var conventionOscillation = {
5396
5631
  const symbolTypes = [...new Set(allSymbols.map((s) => s.symbolType))];
5397
5632
  for (const type of symbolTypes) {
5398
5633
  const typeSymbols = allSymbols.filter((s) => s.symbolType === type);
5399
- if (typeSymbols.length < 3) continue;
5400
- const convCounts = /* @__PURE__ */ new Map();
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
- });
5634
+ const finding = analyzeSymbolTypeConventions(type, typeSymbols);
5635
+ if (finding) findings.push(finding);
5455
5636
  }
5456
5637
  const fileNaming = analyzeFileNaming(ctx.files);
5457
5638
  if (fileNaming) findings.push(fileNaming);
@@ -5666,33 +5847,48 @@ function isInterfaceImplPair(a, b) {
5666
5847
  const bIsInterface = /(?:interface|store|contract|abstract|base|types)\./i.test(b.file) || b.bodyTokenCount < 10;
5667
5848
  return aIsInterface && !bIsInterface || !aIsInterface && bIsInterface;
5668
5849
  }
5669
- function areSemanticallySimlar(a, b) {
5850
+ function isConventionalName(name) {
5851
+ return CONVENTION_NAMES.has(name.toLowerCase());
5852
+ }
5853
+ function areStructurallySimilar(a, b) {
5670
5854
  if (a.domainCategory !== b.domainCategory || a.domainCategory === "general") return false;
5671
5855
  if (a.file === b.file) return false;
5672
5856
  if (Math.abs(a.paramCount - b.paramCount) > 1) return false;
5673
5857
  const sizeRatio = Math.min(a.bodyTokenCount, b.bodyTokenCount) / Math.max(a.bodyTokenCount, b.bodyTokenCount);
5674
- if (sizeRatio < 0.4) return false;
5675
- const nameA = a.name.toLowerCase();
5676
- const nameB = b.name.toLowerCase();
5677
- if (CONVENTION_NAMES.has(nameA) || CONVENTION_NAMES.has(nameB)) return false;
5678
- if (nameA === nameB && /(?:handler|controller|resource|api)/i.test(a.file) && /(?:handler|controller|resource|api)/i.test(b.file)) {
5679
- if (["list", "get", "create", "update", "delete", "find", "search"].includes(nameA)) return false;
5680
- }
5681
- if (nameA === nameB && isInterfaceImplPair(a, b)) return false;
5858
+ return sizeRatio >= 0.4;
5859
+ }
5860
+ function areNamesSimilar(nameA, nameB, hashA, hashB, tokenCountA) {
5861
+ if (nameA === nameB) return true;
5862
+ if (nameA.length >= 6 && nameB.length >= 6 && levenshtein(nameA, nameB) <= 3) return true;
5863
+ if (hashA === hashB && tokenCountA > 20) return true;
5864
+ return false;
5865
+ }
5866
+ function isLayerPair(a, b) {
5682
5867
  const aIsRepo = /(?:repository|repo|store|postgres|mysql|mongo|dal)/i.test(a.file);
5683
5868
  const bIsRepo = /(?:repository|repo|store|postgres|mysql|mongo|dal)/i.test(b.file);
5684
5869
  const aIsHandler = /(?:handler|controller|route|endpoint|api)/i.test(a.file);
5685
5870
  const bIsHandler = /(?:handler|controller|route|endpoint|api)/i.test(b.file);
5686
- if (aIsHandler && bIsRepo || aIsRepo && bIsHandler) return false;
5871
+ if (aIsHandler && bIsRepo || aIsRepo && bIsHandler) return true;
5872
+ const nameA = a.name.toLowerCase();
5873
+ const nameB = b.name.toLowerCase();
5687
5874
  if (nameA === nameB && a.paramCount === b.paramCount) {
5688
- if (aIsHandler && bIsHandler) return false;
5689
- if (aIsRepo && bIsRepo) return false;
5875
+ if (aIsHandler && bIsHandler) return true;
5876
+ if (aIsRepo && bIsRepo) return true;
5690
5877
  }
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
5878
  return false;
5695
5879
  }
5880
+ function areSemanticallySimlar(a, b) {
5881
+ if (!areStructurallySimilar(a, b)) return false;
5882
+ const nameA = a.name.toLowerCase();
5883
+ const nameB = b.name.toLowerCase();
5884
+ if (isConventionalName(nameA) || isConventionalName(nameB)) return false;
5885
+ if (nameA === nameB && /(?:handler|controller|resource|api)/i.test(a.file) && /(?:handler|controller|resource|api)/i.test(b.file)) {
5886
+ if (["list", "get", "create", "update", "delete", "find", "search"].includes(nameA)) return false;
5887
+ }
5888
+ if (nameA === nameB && isInterfaceImplPair(a, b)) return false;
5889
+ if (isLayerPair(a, b)) return false;
5890
+ return areNamesSimilar(nameA, nameB, a.bodyHash, b.bodyHash, a.bodyTokenCount);
5891
+ }
5696
5892
  function levenshtein(a, b) {
5697
5893
  const m = a.length, n = b.length;
5698
5894
  if (m === 0) return n;
@@ -5983,8 +6179,7 @@ function detectUnroutedHandlers(allExports, allRoutes, usage) {
5983
6179
  }
5984
6180
  return findings;
5985
6181
  }
5986
- function detectUnusedTypeScaffolding(files, usage) {
5987
- const findings = [];
6182
+ function extractTypeDefinitions(files) {
5988
6183
  const typeDefinitions = [];
5989
6184
  const goMethodReceivers = /* @__PURE__ */ new Set();
5990
6185
  for (const file of files) {
@@ -6014,6 +6209,9 @@ function detectUnusedTypeScaffolding(files, usage) {
6014
6209
  typeDefinitions.push({ name: typeName, file: file.path, line: i + 1 });
6015
6210
  }
6016
6211
  }
6212
+ return typeDefinitions;
6213
+ }
6214
+ function findUnusedTypes(typeDefinitions, usage) {
6017
6215
  const unusedTypesByFile = /* @__PURE__ */ new Map();
6018
6216
  for (const td of typeDefinitions) {
6019
6217
  const refs = usage.get(td.name);
@@ -6023,6 +6221,12 @@ function detectUnusedTypeScaffolding(files, usage) {
6023
6221
  unusedTypesByFile.get(td.file).push(td);
6024
6222
  }
6025
6223
  }
6224
+ return unusedTypesByFile;
6225
+ }
6226
+ function detectUnusedTypeScaffolding(files, usage) {
6227
+ const findings = [];
6228
+ const typeDefinitions = extractTypeDefinitions(files);
6229
+ const unusedTypesByFile = findUnusedTypes(typeDefinitions, usage);
6026
6230
  for (const [filePath, unusedTypes] of unusedTypesByFile) {
6027
6231
  if (unusedTypes.length < 3) continue;
6028
6232
  findings.push({
@@ -6465,25 +6669,8 @@ function scoreColorFn(score, max) {
6465
6669
  if (ratio >= 0.5) return chalk.yellow;
6466
6670
  return chalk.red;
6467
6671
  }
6468
- function renderTerminalOutput(result) {
6672
+ function renderFindingsList(result) {
6469
6673
  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
6674
  for (const cat of ALL_CATEGORIES) {
6488
6675
  const config = CATEGORY_CONFIG[cat];
6489
6676
  const s = result.scores[cat];
@@ -6517,6 +6704,10 @@ function renderTerminalOutput(result) {
6517
6704
  }
6518
6705
  lines.push("");
6519
6706
  }
6707
+ return lines;
6708
+ }
6709
+ function renderCategoryBars(result) {
6710
+ const lines = [];
6520
6711
  lines.push(chalk.bold("\u2500\u2500 Vibe Debt Score \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6521
6712
  for (const cat of ALL_CATEGORIES) {
6522
6713
  const config = CATEGORY_CONFIG[cat];
@@ -6540,6 +6731,35 @@ function renderTerminalOutput(result) {
6540
6731
  const totalColor = scoreColorFn(result.compositeScore, result.maxCompositeScore);
6541
6732
  lines.push(` ${padRight("Total Score:", 28)} ${totalColor(`${result.compositeScore.toFixed(0)}/${result.maxCompositeScore}`)}`);
6542
6733
  lines.push("");
6734
+ return lines;
6735
+ }
6736
+ function renderScoreSection(result) {
6737
+ const lines = [];
6738
+ const banner = ` VibeDrift v${getVersion()} `;
6739
+ const border = "\u2500".repeat(banner.length);
6740
+ lines.push(chalk.cyan(`\u256D${border}\u256E`));
6741
+ lines.push(chalk.cyan(`\u2502${banner}\u2502`));
6742
+ lines.push(chalk.cyan(`\u2570${border}\u256F`));
6743
+ lines.push("");
6744
+ const langCounts = /* @__PURE__ */ new Map();
6745
+ for (const f of result.context.files) {
6746
+ if (f.language) {
6747
+ const label = getLanguageDisplayName(f.language);
6748
+ langCounts.set(label, (langCounts.get(label) ?? 0) + 1);
6749
+ }
6750
+ }
6751
+ const langStr = [...langCounts.entries()].map(([l, c]) => `${l}: ${c}`).join(", ");
6752
+ lines.push(chalk.dim(`Scanning: ${result.context.rootDir}`));
6753
+ lines.push(chalk.dim(`Files: ${result.context.files.length} (${langStr}) | Lines: ${result.context.totalLines.toLocaleString()} | Time: ${(result.scanTimeMs / 1e3).toFixed(1)}s`));
6754
+ lines.push("");
6755
+ return lines;
6756
+ }
6757
+ function renderTerminalOutput(result) {
6758
+ const lines = [
6759
+ ...renderScoreSection(result),
6760
+ ...renderFindingsList(result),
6761
+ ...renderCategoryBars(result)
6762
+ ];
6543
6763
  if (result.deepInsights.length > 0) {
6544
6764
  lines.push(chalk.bold("\u2500\u2500 Deep Analysis (AI-Powered) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6545
6765
  for (const insight of result.deepInsights.slice(0, 10)) {
@@ -6673,8 +6893,7 @@ function extractIntentPatterns(result) {
6673
6893
  }
6674
6894
  return patterns;
6675
6895
  }
6676
- function buildFileCoherence(result) {
6677
- const entries = [...result.perFileScores.entries()];
6896
+ function buildDriftFileMap(result) {
6678
6897
  const driftFiles = /* @__PURE__ */ new Map();
6679
6898
  for (const d of result.driftFindings ?? []) {
6680
6899
  for (const df of d.deviatingFiles) {
@@ -6683,6 +6902,9 @@ function buildFileCoherence(result) {
6683
6902
  driftFiles.get(df.path).push({ category: catKey, pattern: df.detectedPattern });
6684
6903
  }
6685
6904
  }
6905
+ return driftFiles;
6906
+ }
6907
+ function buildCategorySet(result) {
6686
6908
  const subCatNames = {
6687
6909
  data_access: "Data Access",
6688
6910
  error_handling: "Error Handling",
@@ -6703,9 +6925,12 @@ function buildFileCoherence(result) {
6703
6925
  catSet.set(d.driftCategory, baseCatNames[d.driftCategory]);
6704
6926
  }
6705
6927
  }
6706
- const dna = result.codeDnaResult;
6707
- let projectDominantPattern = "";
6928
+ return catSet;
6929
+ }
6930
+ function buildCodeDnaPatternData(result) {
6708
6931
  const filePatternMap = /* @__PURE__ */ new Map();
6932
+ let projectDominantPattern = "";
6933
+ const dna = result.codeDnaResult;
6709
6934
  if (dna?.patternDistributions?.length > 0) {
6710
6935
  const patternCounts = /* @__PURE__ */ new Map();
6711
6936
  for (const pd of dna.patternDistributions) {
@@ -6721,9 +6946,16 @@ function buildFileCoherence(result) {
6721
6946
  projectDominantPattern = p;
6722
6947
  }
6723
6948
  }
6724
- if (projectDominantPattern) {
6725
- catSet.set("codedna_architecture", "Architecture");
6726
- }
6949
+ }
6950
+ return { projectDominantPattern, filePatternMap };
6951
+ }
6952
+ function buildFileCoherence(result) {
6953
+ const entries = [...result.perFileScores.entries()];
6954
+ const driftFiles = buildDriftFileMap(result);
6955
+ const catSet = buildCategorySet(result);
6956
+ const { projectDominantPattern, filePatternMap } = buildCodeDnaPatternData(result);
6957
+ if (projectDominantPattern) {
6958
+ catSet.set("codedna_architecture", "Architecture");
6727
6959
  }
6728
6960
  const categories = [...catSet.values()];
6729
6961
  const catKeys = [...catSet.keys()];
@@ -6796,9 +7028,7 @@ function buildCoherenceMatrix(files) {
6796
7028
  const colHeaders = categories.map(
6797
7029
  (c) => `<th style="padding:6px 12px;font-size:11px;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:0.5px;text-align:center;font-weight:600">${esc(c)}</th>`
6798
7030
  ).join("");
6799
- function fileRow(f, open) {
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";
7031
+ function buildAlignmentCells(f) {
6802
7032
  const deviations = [];
6803
7033
  const cells = f.alignments.map((a) => {
6804
7034
  if (a.matches) return `<td style="text-align:center;padding:6px 12px;color:var(--intent-green);font-size:14px">&#9679;</td>`;
@@ -6806,7 +7036,16 @@ function buildCoherenceMatrix(files) {
6806
7036
  deviations.push(`${a.category}: ${a.actual ?? "deviates"}`);
6807
7037
  return `<td style="text-align:center;padding:6px 12px;color:${devColor};font-size:13px" data-tooltip="${esc(a.actual ?? "deviates")}">&#9670;</td>`;
6808
7038
  }).join("");
6809
- const summary = 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>`;
7039
+ return { cells, deviations };
7040
+ }
7041
+ function buildDeviationSummary(deviations) {
7042
+ return deviations.length > 0 ? `<td style="padding:6px 12px;font-size:11px;color:var(--text-secondary);max-width:250px">${deviations.map((d) => esc(d)).join("; ")}</td>` : `<td style="padding:6px 12px;font-size:11px;color:var(--intent-green)">Fully aligned</td>`;
7043
+ }
7044
+ function fileRow(f, open) {
7045
+ const pctColor = f.alignmentPct >= 100 ? "var(--intent-green)" : f.alignmentPct >= 50 ? "var(--drift-amber)" : "var(--drift-red)";
7046
+ const bgTint = f.alignmentPct < 50 ? "var(--tint-red)" : f.alignmentPct < 100 ? "var(--tint-amber)" : "transparent";
7047
+ const { cells, deviations } = buildAlignmentCells(f);
7048
+ const summary = buildDeviationSummary(deviations);
6810
7049
  return `<tr style="background:${bgTint}" id="file-${esc(f.path.replace(/[^a-zA-Z0-9]/g, "-"))}">
6811
7050
  <td class="mono" style="padding:6px 12px;font-size:12px;color:var(--text-secondary);white-space:nowrap;max-width:280px;overflow:hidden;text-overflow:ellipsis" data-scroll-to="rank-${esc(f.path.replace(/[^a-zA-Z0-9]/g, "-"))}">${esc(f.path)}</td>
6812
7051
  ${cells}
@@ -7069,6 +7308,66 @@ function buildFixFirst(result) {
7069
7308
  ${cards}
7070
7309
  </section>`;
7071
7310
  }
7311
+ function buildDeviatingBlocks(d) {
7312
+ const isConvention = d.driftCategory === "naming_conventions";
7313
+ const devByPattern = /* @__PURE__ */ new Map();
7314
+ for (const df of d.deviatingFiles) {
7315
+ const key = df.detectedPattern;
7316
+ if (!devByPattern.has(key)) devByPattern.set(key, []);
7317
+ devByPattern.get(key).push(df);
7318
+ }
7319
+ if (isConvention && d.deviatingFiles.length > 4) {
7320
+ return [...devByPattern.entries()].map(([pattern, files]) => {
7321
+ const fileList = files.map((df) => esc(df.path)).join("</div><div style='padding:1px 0'>");
7322
+ const collapseId = `conv-${pattern.replace(/[^a-zA-Z0-9]/g, "-")}-${Math.random().toString(36).slice(2, 6)}`;
7323
+ return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
7324
+ <div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT &mdash; ${esc(pattern)} &mdash; ${files.length} files</div>
7325
+ <div style="cursor:pointer;font-size:12px;color:var(--text-secondary);margin-top:6px" data-collapse="${collapseId}">&#9654; Show ${files.length} files</div>
7326
+ <div id="${collapseId}" style="display:none;padding:6px 0" class="mono" style="font-size:12px;color:var(--text-secondary)"><div style="padding:1px 0">${fileList}</div></div>
7327
+ </div>`;
7328
+ }).join("");
7329
+ }
7330
+ return d.deviatingFiles.slice(0, 4).map((df) => {
7331
+ const evidence = df.evidence.slice(0, 3).map(
7332
+ (e) => `<div style="background:var(--bg-code);padding:6px 12px;border-radius:0;margin:4px 0;overflow-x:auto" class="mono"><span style="color:var(--text-tertiary);margin-right:12px;user-select:none">${e.line}</span>${esc(e.code.slice(0, 120))}</div>`
7333
+ ).join("");
7334
+ return `<div style="background:var(--tint-orange);border-left:3px solid var(--drift-orange);border-radius:0;padding:12px 16px;margin:8px 0">
7335
+ <div class="label" style="color:var(--drift-orange);margin-bottom:4px">DRIFT &mdash; ${esc(df.detectedPattern)}</div>
7336
+ <div class="mono" style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">${esc(df.path)}</div>
7337
+ ${evidence}
7338
+ </div>`;
7339
+ }).join("");
7340
+ }
7341
+ function buildDriftRecommendation(d) {
7342
+ let recText = d.recommendation ?? "";
7343
+ if (recText && d.deviatingFiles.length > 0) {
7344
+ const firstDev = d.deviatingFiles[0];
7345
+ const firstEvidence = firstDev.evidence?.[0];
7346
+ if (recText.includes("Migrate deviating files") || recText.includes("Standardize deviating")) {
7347
+ const fileList = d.deviatingFiles.slice(0, 3).map((f) => f.path.split("/").pop()).join(", ");
7348
+ recText = `In ${fileList}${d.deviatingFiles.length > 3 ? ` (+${d.deviatingFiles.length - 3} more)` : ""}: replace ${firstDev.detectedPattern} with the dominant ${d.dominantPattern} pattern (used by ${d.dominantCount}/${d.totalRelevantFiles} files).`;
7349
+ if (firstEvidence) {
7350
+ recText += ` Example: ${firstDev.path.split("/").pop()}:${firstEvidence.line} currently uses ${firstDev.detectedPattern}.`;
7351
+ }
7352
+ }
7353
+ }
7354
+ return recText;
7355
+ }
7356
+ function buildDistributionBar(d) {
7357
+ const domPct = d.totalRelevantFiles > 0 ? Math.round(d.dominantCount / d.totalRelevantFiles * 100) : 0;
7358
+ const devPct = 100 - domPct;
7359
+ return `<div style="margin:12px 0">
7360
+ <div style="display:flex;height:6px;border-radius:0;overflow:hidden;gap:2px">
7361
+ <div style="width:${domPct}%;background:var(--intent-green);border-radius:0"></div>
7362
+ <div style="width:${devPct}%;background:var(--drift-orange);border-radius:0"></div>
7363
+ </div>
7364
+ <div style="display:flex;gap:16px;margin-top:4px;font-size:11px;color:var(--text-tertiary)">
7365
+ <span>${esc(d.dominantPattern)} (${d.dominantCount})</span>
7366
+ <span>Deviating (${d.totalRelevantFiles - d.dominantCount})</span>
7367
+ <span style="margin-left:auto">${d.consistencyScore}% consistent</span>
7368
+ </div>
7369
+ </div>`;
7370
+ }
7072
7371
  function buildDriftFindings(result) {
7073
7372
  const driftCats = ["architectural_consistency", "security_posture", "semantic_duplication", "naming_conventions", "phantom_scaffolding"];
7074
7373
  const catLabels = {
@@ -7092,61 +7391,9 @@ function buildDriftFindings(result) {
7092
7391
  const domBlock = `<div style="background:var(--tint-green);border-left:3px solid var(--intent-green);border-radius:0;padding:12px 16px;margin:8px 0">
7093
7392
  <div class="label" style="color:var(--intent-green);margin-bottom:6px">INTENT (DOMINANT) &mdash; ${esc(d.dominantPattern)} &mdash; ${d.dominantCount} files${closeSplitNote}</div>
7094
7393
  </div>`;
7095
- const isConvention = d.driftCategory === "naming_conventions";
7096
- const devByPattern = /* @__PURE__ */ new Map();
7097
- for (const df of d.deviatingFiles) {
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 &mdash; ${esc(pattern)} &mdash; ${files.length} files</div>
7109
- <div style="cursor:pointer;font-size:12px;color:var(--text-secondary);margin-top:6px" data-collapse="${collapseId}">&#9654; 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 &mdash; ${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
- }
7394
+ const devBlocks = buildDeviatingBlocks(d);
7395
+ const distBar = buildDistributionBar(d);
7396
+ const recText = buildDriftRecommendation(d);
7150
7397
  const closeSplitQualifier = domPctVal < 70 && domPctVal >= 50 ? `<div style="margin-top:8px;padding:8px 14px;background:var(--tint-amber);border-left:3px solid var(--drift-amber);border-radius:0;font-size:13px;line-height:1.6;color:var(--text-secondary)">
7151
7398
  <strong>${esc(d.dominantPattern)}</strong> is dominant at ${domPctVal}%, but the split is close.
7152
7399
  If your team prefers <strong>${esc(d.deviatingFiles[0]?.detectedPattern ?? "the alternative")}</strong>, adopt that instead.
@@ -7374,6 +7621,37 @@ function buildDeepInsights(result) {
7374
7621
  ${cards}
7375
7622
  </section>`;
7376
7623
  }
7624
+ function buildSequenceCards(similarities) {
7625
+ const seqCards = similarities.slice(0, 6).map((sim) => {
7626
+ const pct = Math.round(sim.similarity * 100);
7627
+ const color = pct >= 90 ? "var(--drift-red)" : "var(--drift-orange)";
7628
+ const matchLabel = pct >= 100 ? "Exact duplicate" : pct >= 90 ? "Near-exact match" : "Similar";
7629
+ return `<div style="display:flex;align-items:center;gap:12px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
7630
+ <span class="mono" style="min-width:40px;font-weight:700;color:${color}">${pct}%</span>
7631
+ <div style="flex:1">
7632
+ <span style="font-size:11px;font-weight:600;color:${color};margin-right:6px">${matchLabel}</span>
7633
+ <span class="mono" style="font-size:12px;color:var(--text-secondary)">${esc(sim.functionA.name)}()</span>
7634
+ <span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionA.relativePath || sim.functionA.file))}</span>
7635
+ <span style="color:var(--text-tertiary);margin:0 4px">&harr;</span>
7636
+ <span class="mono" style="font-size:12px;color:var(--drift-orange)">${esc(sim.functionB.name)}()</span>
7637
+ <span style="font-size:11px;color:var(--text-tertiary)"> in ${esc(shortPath(sim.functionB.relativePath || sim.functionB.file))}</span>
7638
+ </div>
7639
+ </div>`;
7640
+ }).join("");
7641
+ return `<details style="margin-bottom:8px"><summary style="cursor:pointer;font-size:13px;font-weight:500;color:var(--text-primary);list-style:none;padding:6px 0"><span class="chevron">&#9654;</span> Operation sequence matches <span style="color:var(--text-tertiary);font-weight:400">${similarities.length} pairs</span></summary><div style="padding:4px 0 4px 16px">${seqCards}</div></details>`;
7642
+ }
7643
+ function buildDeviationCards(justifications) {
7644
+ const devCards = justifications.map((dj) => {
7645
+ const vColor = dj.verdict === "likely_justified" ? "var(--intent-green)" : dj.verdict === "likely_accidental" ? "var(--drift-red)" : "var(--drift-amber)";
7646
+ const vLabel = dj.verdict === "likely_justified" ? "JUSTIFIED" : dj.verdict === "likely_accidental" ? "ACCIDENTAL" : "UNCERTAIN";
7647
+ return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border-subtle)">
7648
+ <span class="sev-badge" style="background:${vColor};min-width:80px;text-align:center">${vLabel}</span>
7649
+ <span class="mono" style="font-size:12px;color:var(--text-secondary);flex:1">${esc(shortPath(dj.relativePath || dj.file))}</span>
7650
+ <span style="font-size:12px;color:var(--text-tertiary)">${esc(dj.deviatingPattern)} vs ${esc(dj.dominantPattern)}</span>
7651
+ </div>`;
7652
+ }).join("");
7653
+ return `<details style="margin-bottom:8px"><summary style="cursor:pointer;font-size:13px;font-weight:500;color:var(--text-primary);list-style:none;padding:6px 0"><span class="chevron">&#9654;</span> Deviation analysis <span style="color:var(--text-tertiary);font-weight:400">${justifications.length}</span></summary><div style="padding:4px 0 4px 16px">${devCards}</div></details>`;
7654
+ }
7377
7655
  function buildCodeDnaSummary(result) {
7378
7656
  const dna = result.codeDnaResult;
7379
7657
  if (!dna) return "";
@@ -7384,35 +7662,10 @@ function buildCodeDnaSummary(result) {
7384
7662
  if (!hasDupes && !hasSeqs && !hasTaint && !hasDevs) return "";
7385
7663
  const parts = [];
7386
7664
  if (hasSeqs) {
7387
- const seqCards = dna.sequenceSimilarities.slice(0, 6).map((sim) => {
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">&harr;</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">&#9654;</span> Operation sequence matches <span style="color:var(--text-tertiary);font-weight:400">${dna.sequenceSimilarities.length} pairs</span></summary><div style="padding:4px 0 4px 16px">${seqCards}</div></details>`);
7665
+ parts.push(buildSequenceCards(dna.sequenceSimilarities));
7404
7666
  }
7405
7667
  if (hasDevs) {
7406
- const devCards = dna.deviationJustifications.map((dj) => {
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">&#9654;</span> Deviation analysis <span style="color:var(--text-tertiary);font-weight:400">${dna.deviationJustifications.length}</span></summary><div style="padding:4px 0 4px 16px">${devCards}</div></details>`);
7668
+ parts.push(buildDeviationCards(dna.deviationJustifications));
7416
7669
  }
7417
7670
  if (parts.length === 0) return "";
7418
7671
  const totalFindings = dna.findings?.length ?? 0;
@@ -7807,34 +8060,8 @@ function exportCSV() {
7807
8060
  }
7808
8061
 
7809
8062
  // \u2500\u2500\u2500\u2500 Export: DOCX (Word-compatible HTML) \u2500\u2500\u2500\u2500
7810
- function exportDOCX() {
7811
- const d = window.__VIBEDRIFT_DATA;
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 &middot; ' + d.totalLines.toLocaleString() + ' LOC &middot; Score: <strong>' + d.score + '/' + d.maxScore + '</strong></p>';
7835
- html += '</div>';
7836
-
7837
- // Drift findings
8063
+ function buildDocxFindingsHtml(d) {
8064
+ let html = '';
7838
8065
  html += '<h1>Drift Findings</h1>';
7839
8066
  for (const f of d.driftFindings || []) {
7840
8067
  const sev = f.severity === 'error' ? 'critical' : f.severity;
@@ -7844,8 +8071,6 @@ function exportDOCX() {
7844
8071
  html += '<div class="drift"><strong>DEVIATING:</strong> ' + esc2(f.devFiles) + '</div>';
7845
8072
  if (f.recommendation) html += '<div class="rec"><strong>Fix:</strong> ' + esc2(f.recommendation) + '</div>';
7846
8073
  }
7847
-
7848
- // All findings
7849
8074
  html += '<div class="page-break"></div><h1>All Findings</h1>';
7850
8075
  html += '<table><tr><th>Severity</th><th>Analyzer</th><th>Finding</th><th>File</th><th>Line</th></tr>';
7851
8076
  for (const f of d.findings || []) {
@@ -7854,16 +8079,17 @@ function exportDOCX() {
7854
8079
  html += '<td><code>' + esc2(f.file) + '</code></td><td>' + f.line + '</td></tr>';
7855
8080
  }
7856
8081
  html += '</table>';
8082
+ return html;
8083
+ }
7857
8084
 
7858
- // File scores
8085
+ function buildDocxScoresHtml(d) {
8086
+ let html = '';
7859
8087
  html += '<div class="page-break"></div><h1>File Scores</h1>';
7860
8088
  html += '<table><tr><th>File</th><th>Score</th><th>Findings</th></tr>';
7861
8089
  for (const f of d.fileScores || []) {
7862
8090
  html += '<tr><td><code>' + esc2(f.file) + '</code></td><td>' + f.score + '/100</td><td>' + f.findings + '</td></tr>';
7863
8091
  }
7864
8092
  html += '</table>';
7865
-
7866
- // Code DNA
7867
8093
  if (d.codeDna) {
7868
8094
  html += '<div class="page-break"></div><h1>Code DNA Analysis</h1>';
7869
8095
  if (d.codeDna.sequences && d.codeDna.sequences.length > 0) {
@@ -7877,8 +8103,6 @@ function exportDOCX() {
7877
8103
  html += '</table>';
7878
8104
  }
7879
8105
  }
7880
-
7881
- // Deep insights
7882
8106
  if (d.deepInsights && d.deepInsights.length > 0) {
7883
8107
  html += '<div class="page-break"></div><h1>Deep Analysis Insights (AI-Powered)</h1>';
7884
8108
  for (const ins of d.deepInsights) {
@@ -7887,6 +8111,37 @@ function exportDOCX() {
7887
8111
  if (ins.recommendation) html += '<div class="rec"><strong>Fix:</strong> ' + esc2(ins.recommendation) + '</div>';
7888
8112
  }
7889
8113
  }
8114
+ return html;
8115
+ }
8116
+
8117
+ function exportDOCX() {
8118
+ const d = window.__VIBEDRIFT_DATA;
8119
+ if (!d) { alert('Report data not available'); return; }
8120
+
8121
+ let html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word"><head><meta charset="utf-8"><style>';
8122
+ html += 'body{font-family:Calibri,sans-serif;font-size:11pt;color:#222;margin:40px}';
8123
+ html += 'h1{font-size:20pt;color:#0B0F15;border-bottom:2px solid #00D4FF;padding-bottom:8px;margin-top:28px}';
8124
+ html += 'h2{font-size:14pt;color:#333;margin-top:20px}';
8125
+ html += 'h3{font-size:12pt;color:#555;margin-top:14px}';
8126
+ html += 'table{border-collapse:collapse;width:100%;margin:10px 0}';
8127
+ html += 'th,td{border:1px solid #ddd;padding:6px 10px;text-align:left;font-size:10pt}';
8128
+ html += 'th{background:#f0f4f8;font-weight:600}';
8129
+ html += '.sev-critical{color:#dc2626;font-weight:700} .sev-warning{color:#d97706;font-weight:700} .sev-info{color:#2563eb}';
8130
+ html += '.intent{background:#ecfdf5;border-left:3px solid #10b981;padding:8px 12px;margin:6px 0}';
8131
+ html += '.drift{background:#fff7ed;border-left:3px solid #f97316;padding:8px 12px;margin:6px 0}';
8132
+ html += '.rec{background:#f0f9ff;border-left:3px solid #0ea5e9;padding:8px 12px;margin:6px 0}';
8133
+ html += 'code{font-family:Consolas,monospace;font-size:9pt;background:#f5f5f5;padding:1px 4px}';
8134
+ html += '.page-break{page-break-before:always}';
8135
+ html += '</style></head><body>';
8136
+
8137
+ html += '<div style="text-align:center;margin-bottom:30px">';
8138
+ html += '<h1 style="border:none;font-size:28pt;color:#00D4FF">VIBEDRIFT REPORT</h1>';
8139
+ html += '<p style="font-size:16pt;color:#333">' + esc2(d.project) + '</p>';
8140
+ html += '<p>' + d.fileCount + ' files &middot; ' + d.totalLines.toLocaleString() + ' LOC &middot; Score: <strong>' + d.score + '/' + d.maxScore + '</strong></p>';
8141
+ html += '</div>';
8142
+
8143
+ html += buildDocxFindingsHtml(d);
8144
+ html += buildDocxScoresHtml(d);
7890
8145
 
7891
8146
  html += '<hr><p style="color:#999;font-size:9pt">Generated by VibeDrift v' + d.version + ' | ' + d.fileCount + ' files | No data sent externally</p>';
7892
8147
  html += '</body></html>';
@@ -8305,9 +8560,8 @@ async function resolveAuthAndBanner(options) {
8305
8560
  }
8306
8561
  return { bearerToken, apiUrl };
8307
8562
  }
8308
- async function runAnalysisPipeline(rootDir, options, spinner) {
8563
+ async function discoverAndFilterFiles(rootDir, options, spinner) {
8309
8564
  const isTerminal = options.format === "terminal" && !options.json;
8310
- const timings = {};
8311
8565
  const t0 = Date.now();
8312
8566
  const { ctx, warnings } = await buildAnalysisContext(rootDir);
8313
8567
  const includes = options.include ?? [];
@@ -8321,7 +8575,7 @@ async function runAnalysisPipeline(rootDir, options, spinner) {
8321
8575
  console.error(chalk2.dim(`[filter] ${before} \u2192 ${filtered.length} files after include/exclude`));
8322
8576
  }
8323
8577
  }
8324
- timings.discovery = Date.now() - t0;
8578
+ const discoveryMs = Date.now() - t0;
8325
8579
  if (isTerminal) {
8326
8580
  if (warnings.truncated) {
8327
8581
  console.warn(chalk2.yellow(`
@@ -8339,6 +8593,13 @@ Warning: File limit reached (${warnings.truncatedAt}). Only partial coverage \u2
8339
8593
  console.log("No source files found to analyze.");
8340
8594
  process.exit(0);
8341
8595
  }
8596
+ return { ctx, discoveryMs };
8597
+ }
8598
+ async function runAnalysisPipeline(rootDir, options, spinner) {
8599
+ const isTerminal = options.format === "terminal" && !options.json;
8600
+ const timings = {};
8601
+ const { ctx, discoveryMs } = await discoverAndFilterFiles(rootDir, options, spinner);
8602
+ timings.discovery = discoveryMs;
8342
8603
  if (spinner) spinner.text = `Parsing ${ctx.files.length} files...`;
8343
8604
  const t1 = Date.now();
8344
8605
  await parseFiles(ctx.files);
@@ -8521,6 +8782,12 @@ async function logAndRender(result, options, bearerToken, apiUrl, rootDir, codeD
8521
8782
  }
8522
8783
  }
8523
8784
  const format = options.format ?? (options.json ? "json" : "html");
8785
+ await renderToFormat(result, format, options);
8786
+ if (options.failOnScore !== void 0 && compositeScore < options.failOnScore) {
8787
+ process.exit(1);
8788
+ }
8789
+ }
8790
+ async function renderToFormat(result, format, options) {
8524
8791
  if (format === "html") {
8525
8792
  const html = renderHtmlReport(result);
8526
8793
  const outputPath = options.output ?? "vibedrift-report.html";
@@ -8569,9 +8836,6 @@ async function logAndRender(result, options, bearerToken, apiUrl, rootDir, codeD
8569
8836
  } else {
8570
8837
  console.log(renderTerminalOutput(result));
8571
8838
  }
8572
- if (options.failOnScore !== void 0 && compositeScore < options.failOnScore) {
8573
- process.exit(1);
8574
- }
8575
8839
  }
8576
8840
  async function runScan(targetPath, options) {
8577
8841
  const rootDir = resolve(targetPath);
@@ -8786,58 +9050,7 @@ async function runLogin(options = {}) {
8786
9050
  }
8787
9051
  if (result.status === "pending") continue;
8788
9052
  if (result.status === "authorized") {
8789
- await patchConfig({
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
- }
9053
+ await handleLoginSuccess(result, options);
8841
9054
  return;
8842
9055
  }
8843
9056
  if (result.status === "denied") {
@@ -8855,6 +9068,60 @@ async function runLogin(options = {}) {
8855
9068
  console.error(chalk4.dim(" Run `vibedrift login` again to retry.\n"));
8856
9069
  process.exit(1);
8857
9070
  }
9071
+ async function handleLoginSuccess(result, options) {
9072
+ await patchConfig({
9073
+ token: result.access_token,
9074
+ email: result.email,
9075
+ plan: result.plan,
9076
+ expiresAt: result.expires_at,
9077
+ loggedInAt: (/* @__PURE__ */ new Date()).toISOString(),
9078
+ apiUrl: options.apiUrl
9079
+ });
9080
+ console.log(chalk4.green(" \u2713 Logged in successfully."));
9081
+ console.log("");
9082
+ console.log(` Account: ${chalk4.bold(result.email)}`);
9083
+ console.log(` Plan: ${chalk4.bold(result.plan)}`);
9084
+ console.log("");
9085
+ try {
9086
+ const credits = await fetchCredits(result.access_token, {
9087
+ apiUrl: options.apiUrl
9088
+ });
9089
+ if (credits.has_free_deep_scan && !credits.unlimited) {
9090
+ console.log(
9091
+ chalk4.bgYellow.black.bold(" \u{1F381} 1 FREE deep scan included with your account ")
9092
+ );
9093
+ console.log("");
9094
+ console.log(
9095
+ chalk4.yellow(" Try the full pipeline (Claude analysis, security review,")
9096
+ );
9097
+ console.log(
9098
+ chalk4.yellow(" AI-powered drift detection) on any project \u2014 no card needed.")
9099
+ );
9100
+ console.log("");
9101
+ console.log(` ${chalk4.cyan("vibedrift . --deep")}`);
9102
+ console.log("");
9103
+ } else if (credits.unlimited) {
9104
+ console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
9105
+ console.log("");
9106
+ } else if (credits.available_total > 0) {
9107
+ console.log(
9108
+ chalk4.dim(` You have ${credits.available_total} deep scan credit${credits.available_total === 1 ? "" : "s"} available.`)
9109
+ );
9110
+ console.log(chalk4.dim(" Run `vibedrift . --deep` to use one."));
9111
+ console.log("");
9112
+ } else {
9113
+ console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
9114
+ console.log("");
9115
+ }
9116
+ } catch {
9117
+ if (result.plan === "free") {
9118
+ console.log(chalk4.dim(" Run `vibedrift upgrade` to enable deep AI scans."));
9119
+ } else {
9120
+ console.log(chalk4.dim(" Run `vibedrift . --deep` to use AI-powered analysis."));
9121
+ }
9122
+ console.log("");
9123
+ }
9124
+ }
8858
9125
  function fail(intro, err) {
8859
9126
  const msg = err instanceof VibeDriftApiError ? `${err.status ? `HTTP ${err.status}: ` : ""}${err.message}` : err instanceof Error ? err.message : String(err);
8860
9127
  console.error(chalk4.red(`
@@ -9102,58 +9369,8 @@ import { homedir as homedir3, platform, arch } from "os";
9102
9369
  import { join as join7 } from "path";
9103
9370
  import { stat as stat4, access, constants } from "fs/promises";
9104
9371
  init_version();
9105
- async function runDoctor() {
9106
- let failures = 0;
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("");
9372
+ async function checkAuthStatus() {
9373
+ let authFailures = 0;
9157
9374
  console.log(chalk10.bold(" Authentication"));
9158
9375
  const config = await readConfig();
9159
9376
  const resolved = await resolveToken();
@@ -9170,7 +9387,7 @@ async function runDoctor() {
9170
9387
  const now = Date.now();
9171
9388
  if (expires < now) {
9172
9389
  bad(`Token expired ${Math.floor((now - expires) / 864e5)} days ago`);
9173
- failures++;
9390
+ authFailures++;
9174
9391
  } else {
9175
9392
  ok("Token expires", `${config.expiresAt} (${Math.ceil((expires - now) / 864e5)} days)`);
9176
9393
  }
@@ -9178,6 +9395,10 @@ async function runDoctor() {
9178
9395
  }
9179
9396
  }
9180
9397
  console.log("");
9398
+ return { resolved, authFailures };
9399
+ }
9400
+ async function checkApiConnectivity(resolved) {
9401
+ let failures = 0;
9181
9402
  console.log(chalk10.bold(" API"));
9182
9403
  const apiUrl = await resolveApiUrl();
9183
9404
  ok("API URL", apiUrl);
@@ -9212,6 +9433,64 @@ async function runDoctor() {
9212
9433
  }
9213
9434
  }
9214
9435
  console.log("");
9436
+ return failures;
9437
+ }
9438
+ async function runDoctor() {
9439
+ let failures = 0;
9440
+ console.log("");
9441
+ console.log(chalk10.bold(" VibeDrift Doctor"));
9442
+ console.log("");
9443
+ console.log(chalk10.bold(" Environment"));
9444
+ ok("CLI version", getVersion());
9445
+ ok("Node", process.version);
9446
+ ok("Platform", `${platform()} ${arch()}`);
9447
+ ok("HOME", homedir3());
9448
+ console.log("");
9449
+ console.log(chalk10.bold(" Config"));
9450
+ const configDir = getConfigDir();
9451
+ const configPath = getConfigPath();
9452
+ let configDirOk = false;
9453
+ try {
9454
+ const info2 = await stat4(configDir);
9455
+ if (info2.isDirectory()) {
9456
+ configDirOk = true;
9457
+ const mode = (info2.mode & 511).toString(8);
9458
+ ok("Config dir", `${configDir} (mode ${mode})`);
9459
+ } else {
9460
+ bad(`Config dir exists but is not a directory: ${configDir}`);
9461
+ failures++;
9462
+ }
9463
+ } catch {
9464
+ info("Config dir", `${configDir} (will be created on first login)`);
9465
+ configDirOk = true;
9466
+ }
9467
+ if (configDirOk) {
9468
+ try {
9469
+ await access(configPath, constants.R_OK);
9470
+ const info2 = await stat4(configPath);
9471
+ const mode = (info2.mode & 511).toString(8);
9472
+ if ((info2.mode & 63) !== 0) {
9473
+ warn("Config file", `${configPath} (mode ${mode}, world/group readable \u2014 should be 600)`);
9474
+ } else {
9475
+ ok("Config file", `${configPath} (mode ${mode})`);
9476
+ }
9477
+ } catch {
9478
+ info("Config file", "absent (not logged in)");
9479
+ }
9480
+ }
9481
+ const historyDir = join7(homedir3(), ".vibedrift", "scans");
9482
+ try {
9483
+ const info2 = await stat4(historyDir);
9484
+ if (info2.isDirectory()) ok("Scan history", historyDir);
9485
+ else warn("Scan history", `${historyDir} exists but is not a directory`);
9486
+ } catch {
9487
+ info("Scan history", "empty (no scans run yet)");
9488
+ }
9489
+ console.log("");
9490
+ const { resolved, authFailures } = await checkAuthStatus();
9491
+ failures += authFailures;
9492
+ const apiFailures = await checkApiConnectivity(resolved);
9493
+ failures += apiFailures;
9215
9494
  if (failures === 0) {
9216
9495
  console.log(chalk10.green(" \u2713 All checks passed."));
9217
9496
  } else {