dep-brain 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,8 +31,8 @@ export function renderConsoleReport(result) {
31
31
  ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
32
32
  : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
33
33
  appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
34
- ? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]`
35
- : `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]`, item.confidence, item.explanation, item.recommendation)));
34
+ ? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`
35
+ : `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
36
36
  appendSection(lines, "Policy reasons", result.policy.reasons);
37
37
  if (result.suggestions.length > 0) {
38
38
  lines.push("");
@@ -43,6 +43,13 @@ export function renderConsoleReport(result) {
43
43
  }
44
44
  return lines.join("\n");
45
45
  }
46
+ function formatTransitiveRiskSuffix(item) {
47
+ if (item.riskyTransitiveDeps.length === 0) {
48
+ return "";
49
+ }
50
+ const names = item.riskyTransitiveDeps.slice(0, 3).map((entry) => entry.name).join(", ");
51
+ return ` [transitive score ${item.transitiveRiskScore}] [via ${names}]`;
52
+ }
46
53
  function summaryLine(label, count) {
47
54
  const indicator = count === 0 ? "OK" : "WARN";
48
55
  return `${indicator} ${label}: ${count}`;
@@ -0,0 +1,2 @@
1
+ import type { AnalysisResult } from "../core/analyzer.js";
2
+ export declare function renderDashboardReport(result: AnalysisResult): string;
@@ -0,0 +1,123 @@
1
+ export function renderDashboardReport(result) {
2
+ const topIssues = result.topIssues.map(renderTopIssue).join("");
3
+ const suggestions = result.suggestions.map((item) => `<li>${escapeHtml(item)}</li>`).join("");
4
+ const transitiveHotspots = result.risks
5
+ .filter((item) => item.riskyTransitiveDeps.length > 0)
6
+ .sort((left, right) => right.transitiveRiskScore - left.transitiveRiskScore)
7
+ .map(renderTransitiveHotspot)
8
+ .join("");
9
+ return [
10
+ "<!doctype html>",
11
+ '<html lang="en">',
12
+ "<head>",
13
+ '<meta charset="utf-8">',
14
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
15
+ "<title>Dependency Brain Dashboard</title>",
16
+ "<style>",
17
+ "body{font-family:Arial,sans-serif;margin:0;color:#172033;background:#f6f8fb}",
18
+ "main{max-width:1120px;margin:0 auto;padding:32px}",
19
+ "header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}",
20
+ "h1{font-size:28px;margin:0 0 8px}",
21
+ "h2{font-size:18px;margin:0 0 12px}",
22
+ "h3{font-size:15px;margin:0 0 8px}",
23
+ ".muted{color:#637083;font-size:13px}",
24
+ ".score{font-size:48px;font-weight:700}",
25
+ ".grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:20px}",
26
+ ".split{display:grid;grid-template-columns:1.2fr .8fr;gap:16px;margin-top:16px}",
27
+ ".panel{background:#fff;border:1px solid #dce3ee;border-radius:8px;padding:16px}",
28
+ ".metric{font-size:28px;font-weight:700;margin-top:4px}",
29
+ ".pass{color:#167a43}.fail{color:#b42318}",
30
+ "ol,ul{margin:0;padding-left:20px}",
31
+ "li{margin:8px 0}",
32
+ ".issue{margin-bottom:10px}",
33
+ ".kind{font-size:12px;text-transform:uppercase;color:#637083}",
34
+ ".hotspot{border-top:1px solid #e7ecf4;padding-top:12px;margin-top:12px}",
35
+ ".path{font-family:Consolas,monospace;font-size:12px;background:#f3f6fb;border-radius:6px;padding:6px 8px;margin:6px 0}",
36
+ "@media(max-width:760px){main{padding:20px}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.split{grid-template-columns:1fr}header{display:block}.score{font-size:40px}}",
37
+ "</style>",
38
+ "</head>",
39
+ "<body>",
40
+ "<main>",
41
+ "<header>",
42
+ "<div>",
43
+ "<h1>Dependency Brain Dashboard</h1>",
44
+ `<div class="muted">${escapeHtml(result.rootDir)}</div>`,
45
+ "</div>",
46
+ `<div class="${result.policy.passed ? "pass" : "fail"} score">${result.score}/100</div>`,
47
+ "</header>",
48
+ '<section class="grid">',
49
+ renderMetric("Duplicates", result.duplicates.length),
50
+ renderMetric("Unused", result.unused.length),
51
+ renderMetric("Outdated", result.outdated.length),
52
+ renderMetric("Risks", result.risks.length),
53
+ "</section>",
54
+ '<section class="split">',
55
+ '<div class="panel">',
56
+ "<h2>Policy</h2>",
57
+ `<p class="${result.policy.passed ? "pass" : "fail"}">${result.policy.passed ? "Passed" : "Failed"}</p>`,
58
+ result.policy.reasons.length > 0
59
+ ? `<ul>${result.policy.reasons.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`
60
+ : '<p class="muted">No policy failures.</p>',
61
+ "</div>",
62
+ '<div class="panel">',
63
+ "<h2>Risk Snapshot</h2>",
64
+ `<div class="muted">${result.risks.filter((item) => item.riskyTransitiveDeps.length > 0).length} direct dependencies carry transitive risk.</div>`,
65
+ `<div class="metric">${result.risks.reduce((total, item) => total + item.transitiveRiskScore, 0)}</div>`,
66
+ '<div class="muted">Total transitive risk score</div>',
67
+ "</div>",
68
+ "</section>",
69
+ '<section class="split">',
70
+ '<div class="panel">',
71
+ "<h2>Top Issues</h2>",
72
+ topIssues.length > 0 ? `<ol>${topIssues}</ol>` : '<p class="muted">No actionable issues found.</p>',
73
+ "</div>",
74
+ '<div class="panel">',
75
+ "<h2>Suggestions</h2>",
76
+ suggestions.length > 0 ? `<ul>${suggestions}</ul>` : '<p class="muted">No suggestions.</p>',
77
+ "</div>",
78
+ "</section>",
79
+ '<section class="panel">',
80
+ "<h2>Transitive Risk Hotspots</h2>",
81
+ transitiveHotspots.length > 0
82
+ ? transitiveHotspots
83
+ : '<p class="muted">No transitive risk hotspots found.</p>',
84
+ "</section>",
85
+ "</main>",
86
+ "</body>",
87
+ "</html>"
88
+ ].join("\n");
89
+ }
90
+ function renderMetric(label, value) {
91
+ return `<div class="panel"><div class="muted">${escapeHtml(label)}</div><div class="metric">${value}</div></div>`;
92
+ }
93
+ function renderTopIssue(item) {
94
+ return [
95
+ '<li class="issue">',
96
+ `<div class="kind">${escapeHtml(item.kind)} ${escapeHtml(item.priority)}</div>`,
97
+ `<strong>${escapeHtml(item.name)}</strong>`,
98
+ `<div>${escapeHtml(item.recommendation.summary)}</div>`,
99
+ "</li>"
100
+ ].join("");
101
+ }
102
+ function renderTransitiveHotspot(item) {
103
+ return [
104
+ '<div class="hotspot">',
105
+ `<h3>${escapeHtml(item.name)} <span class="muted">score ${item.transitiveRiskScore}</span></h3>`,
106
+ `<div class="muted">${item.riskFactors.transitiveDependencyCount} transitive dependencies, ${item.riskyTransitiveDeps.length} risky transitive dependencies</div>`,
107
+ "<ul>",
108
+ item.riskyTransitiveDeps
109
+ .slice(0, 4)
110
+ .map((entry) => `<li><strong>${escapeHtml(entry.name)}</strong> [${escapeHtml(entry.trustScore.toUpperCase())}]<div>${escapeHtml(entry.reasons.join("; "))}</div>${entry.introducedByPaths.map((trace) => `<div class="path">${escapeHtml(trace)}</div>`).join("")}</li>`)
111
+ .join(""),
112
+ "</ul>",
113
+ "</div>"
114
+ ].join("");
115
+ }
116
+ function escapeHtml(value) {
117
+ return value
118
+ .replace(/&/g, "&amp;")
119
+ .replace(/</g, "&lt;")
120
+ .replace(/>/g, "&gt;")
121
+ .replace(/"/g, "&quot;")
122
+ .replace(/'/g, "&#39;");
123
+ }
@@ -35,8 +35,8 @@ export function renderMarkdownReport(result) {
35
35
  ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
36
36
  : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
37
37
  appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
38
- ? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]`
39
- : `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]`, item.confidence, item.explanation, item.recommendation)));
38
+ ? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`
39
+ : `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
40
40
  appendSection(lines, "Policy reasons", result.policy.reasons);
41
41
  if (result.suggestions.length > 0) {
42
42
  lines.push("## Suggestions");
@@ -47,6 +47,13 @@ export function renderMarkdownReport(result) {
47
47
  }
48
48
  return lines.join("\n");
49
49
  }
50
+ function formatTransitiveRiskSuffix(item) {
51
+ if (item.riskyTransitiveDeps.length === 0) {
52
+ return "";
53
+ }
54
+ const names = item.riskyTransitiveDeps.slice(0, 3).map((entry) => entry.name).join(", ");
55
+ return ` [transitive score ${item.transitiveRiskScore}] [via ${names}]`;
56
+ }
50
57
  function appendSection(lines, title, items) {
51
58
  if (items.length === 0) {
52
59
  return;
@@ -26,6 +26,11 @@ export interface DepBrainConfig {
26
26
  risk: {
27
27
  transitiveBloatThreshold: number;
28
28
  typosquattingDistanceThreshold: number;
29
+ staleReleaseDays: number;
30
+ agingReleaseDays: number;
31
+ lowDownloadThreshold: number;
32
+ lowTrustWeightThreshold: number;
33
+ mediumTrustWeightThreshold: number;
29
34
  };
30
35
  dashboard: {
31
36
  outputPath: string;
@@ -27,7 +27,12 @@ export const defaultConfig = {
27
27
  },
28
28
  risk: {
29
29
  transitiveBloatThreshold: 50,
30
- typosquattingDistanceThreshold: 2
30
+ typosquattingDistanceThreshold: 2,
31
+ staleReleaseDays: 730,
32
+ agingReleaseDays: 365,
33
+ lowDownloadThreshold: 1000,
34
+ lowTrustWeightThreshold: 6,
35
+ mediumTrustWeightThreshold: 3
31
36
  },
32
37
  dashboard: {
33
38
  outputPath: "depbrain-dashboard.html"
@@ -84,7 +89,12 @@ function normalizeConfig(loaded) {
84
89
  },
85
90
  risk: {
86
91
  transitiveBloatThreshold: normalizeNumber(loaded.risk?.transitiveBloatThreshold, defaultConfig.risk.transitiveBloatThreshold),
87
- typosquattingDistanceThreshold: normalizeNumber(loaded.risk?.typosquattingDistanceThreshold, defaultConfig.risk.typosquattingDistanceThreshold)
92
+ typosquattingDistanceThreshold: normalizeNumber(loaded.risk?.typosquattingDistanceThreshold, defaultConfig.risk.typosquattingDistanceThreshold),
93
+ staleReleaseDays: normalizeNumber(loaded.risk?.staleReleaseDays, defaultConfig.risk.staleReleaseDays),
94
+ agingReleaseDays: normalizeNumber(loaded.risk?.agingReleaseDays, defaultConfig.risk.agingReleaseDays),
95
+ lowDownloadThreshold: normalizeNumber(loaded.risk?.lowDownloadThreshold, defaultConfig.risk.lowDownloadThreshold),
96
+ lowTrustWeightThreshold: normalizeNumber(loaded.risk?.lowTrustWeightThreshold, defaultConfig.risk.lowTrustWeightThreshold),
97
+ mediumTrustWeightThreshold: normalizeNumber(loaded.risk?.mediumTrustWeightThreshold, defaultConfig.risk.mediumTrustWeightThreshold)
88
98
  },
89
99
  dashboard: {
90
100
  outputPath: normalizeString(loaded.dashboard?.outputPath, defaultConfig.dashboard.outputPath)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI and library for explainable dependency intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",