dep-brain 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,32 +7,29 @@ export async function buildDependencyGraph(rootDir) {
7
7
  const pnpmLockfilePath = path.join(rootDir, "pnpm-lock.yaml");
8
8
  const yarnLockfilePath = path.join(rootDir, "yarn.lock");
9
9
  const packageJson = await readJsonFile(packageJsonPath);
10
- const lockPackages = new Map();
11
10
  try {
12
11
  const packageLock = await readJsonFile(lockfilePath);
13
- for (const [packagePath, details] of Object.entries(packageLock.packages ?? {})) {
14
- const name = extractPackageName(packagePath);
15
- const version = details.version;
16
- if (!name || !version) {
17
- continue;
18
- }
19
- const instances = lockPackages.get(name) ?? new Map();
20
- const normalizedPath = packagePath || "node_modules/" + name;
21
- instances.set(normalizedPath, { path: normalizedPath, version });
22
- lockPackages.set(name, instances);
23
- }
24
- for (const [name, details] of Object.entries(packageLock.dependencies ?? {})) {
25
- if (!details.version) {
26
- continue;
27
- }
28
- const instances = lockPackages.get(name) ?? new Map();
29
- const normalizedPath = `node_modules/${name}`;
30
- instances.set(normalizedPath, { path: normalizedPath, version: details.version });
31
- lockPackages.set(name, instances);
32
- }
12
+ const parsed = parseNpmLockfile(packageLock, {
13
+ ...packageJson.dependencies,
14
+ ...packageJson.devDependencies
15
+ });
16
+ return {
17
+ rootDir,
18
+ packageJsonPath,
19
+ lockfilePath,
20
+ dependencies: packageJson.dependencies ?? {},
21
+ devDependencies: packageJson.devDependencies ?? {},
22
+ overrides: packageJson.overrides ?? {},
23
+ scripts: packageJson.scripts ?? {},
24
+ lockPackages: parsed.lockPackages,
25
+ lockDependencies: parsed.lockDependencies
26
+ };
33
27
  }
34
28
  catch {
35
- const fallbackLockfile = await readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath);
29
+ const fallbackLockfile = await readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath, {
30
+ ...packageJson.dependencies,
31
+ ...packageJson.devDependencies
32
+ });
36
33
  return {
37
34
  rootDir,
38
35
  packageJsonPath,
@@ -41,29 +38,19 @@ export async function buildDependencyGraph(rootDir) {
41
38
  devDependencies: packageJson.devDependencies ?? {},
42
39
  overrides: packageJson.overrides ?? {},
43
40
  scripts: packageJson.scripts ?? {},
44
- lockPackages: fallbackLockfile.lockPackages
41
+ lockPackages: fallbackLockfile.lockPackages,
42
+ lockDependencies: fallbackLockfile.lockDependencies
45
43
  };
46
44
  }
47
- return {
48
- rootDir,
49
- packageJsonPath,
50
- lockfilePath,
51
- dependencies: packageJson.dependencies ?? {},
52
- devDependencies: packageJson.devDependencies ?? {},
53
- overrides: packageJson.overrides ?? {},
54
- scripts: packageJson.scripts ?? {},
55
- lockPackages: Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
56
- name,
57
- Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
58
- ]))
59
- };
60
45
  }
61
- async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath) {
46
+ async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath, rootDependencies) {
62
47
  try {
63
48
  const content = await fs.readFile(pnpmLockfilePath, "utf8");
49
+ const parsed = parsePnpmLockfile(content, rootDependencies);
64
50
  return {
65
51
  lockfilePath: pnpmLockfilePath,
66
- lockPackages: parsePnpmLockfile(content)
52
+ lockPackages: parsed.lockPackages,
53
+ lockDependencies: parsed.lockDependencies
67
54
  };
68
55
  }
69
56
  catch {
@@ -71,58 +58,150 @@ async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath) {
71
58
  }
72
59
  try {
73
60
  const content = await fs.readFile(yarnLockfilePath, "utf8");
61
+ const parsed = parseYarnLockfile(content, rootDependencies);
74
62
  return {
75
63
  lockfilePath: yarnLockfilePath,
76
- lockPackages: parseYarnLockfile(content)
64
+ lockPackages: parsed.lockPackages,
65
+ lockDependencies: parsed.lockDependencies
77
66
  };
78
67
  }
79
68
  catch {
80
69
  return {
81
- lockPackages: {}
70
+ lockPackages: {},
71
+ lockDependencies: {}
82
72
  };
83
73
  }
84
74
  }
85
- function extractPackageName(packagePath) {
86
- if (!packagePath) {
87
- return null;
75
+ function parseNpmLockfile(packageLock, rootDependencies) {
76
+ const lockPackages = new Map();
77
+ const lockDependencies = new Map();
78
+ for (const [packagePath, details] of Object.entries(packageLock.packages ?? {})) {
79
+ const name = details.name ?? extractPackageName(packagePath);
80
+ const version = details.version;
81
+ if (name && version) {
82
+ const instances = lockPackages.get(name) ?? new Map();
83
+ const normalizedPath = packagePath || "node_modules/" + name;
84
+ instances.set(normalizedPath, { path: normalizedPath, version });
85
+ lockPackages.set(name, instances);
86
+ }
87
+ if (name) {
88
+ addDependencyNames(lockDependencies, name, Object.keys(details.dependencies ?? {}));
89
+ }
88
90
  }
89
- const match = packagePath.match(/(?:^|\/)node_modules\/(.+)$/);
90
- if (!match) {
91
- return null;
91
+ for (const [name, details] of Object.entries(packageLock.dependencies ?? {})) {
92
+ if (details.version) {
93
+ const instances = lockPackages.get(name) ?? new Map();
94
+ const normalizedPath = `node_modules/${name}`;
95
+ instances.set(normalizedPath, { path: normalizedPath, version: details.version });
96
+ lockPackages.set(name, instances);
97
+ }
98
+ addDependencyNames(lockDependencies, name, Object.keys(details.requires ?? {}));
92
99
  }
93
- return match[1];
100
+ addDependencyNames(lockDependencies, "__root__", Object.keys(rootDependencies));
101
+ return {
102
+ lockPackages: toLockPackageRecord(lockPackages),
103
+ lockDependencies: toDependencyRecord(lockDependencies)
104
+ };
94
105
  }
95
- function parsePnpmLockfile(content) {
106
+ function parsePnpmLockfile(content, rootDependencies) {
96
107
  const lockPackages = new Map();
97
- for (const line of content.split(/\r?\n/)) {
98
- const match = line.match(/^\s{2}(?:'|")?\/((?:@[^/]+\/)?[^/@'"]+)@([^('":]+)[^:]*:(?:'|")?\s*$/);
99
- if (!match) {
108
+ const lockDependencies = new Map();
109
+ const lines = content.split(/\r?\n/);
110
+ let currentName = null;
111
+ let currentVersion = null;
112
+ let inDependenciesBlock = false;
113
+ for (const line of lines) {
114
+ const packageMatch = line.match(/^\s{2}(?:'|")?\/((?:@[^/]+\/)?[^/@'"]+)@([^('":]+)[^:]*:(?:'|")?\s*$/);
115
+ if (packageMatch) {
116
+ currentName = packageMatch[1];
117
+ currentVersion = packageMatch[2];
118
+ inDependenciesBlock = false;
119
+ addLockPackage(lockPackages, currentName, `pnpm:${currentName}@${currentVersion}`, currentVersion);
120
+ continue;
121
+ }
122
+ if (!currentName) {
123
+ continue;
124
+ }
125
+ if (/^\s{4}(?:dependencies|optionalDependencies):\s*$/.test(line)) {
126
+ inDependenciesBlock = true;
127
+ continue;
128
+ }
129
+ if (/^\s{4}\S/.test(line) && !/^\s{4}(?:dependencies|optionalDependencies):\s*$/.test(line)) {
130
+ inDependenciesBlock = false;
131
+ }
132
+ if (!inDependenciesBlock) {
100
133
  continue;
101
134
  }
102
- addLockPackage(lockPackages, match[1], `pnpm-lock:${match[0].trim()}`, match[2]);
135
+ const dependencyMatch = line.match(/^\s{6}((?:@[^/]+\/)?[^:\s]+):\s*(.+)?$/);
136
+ if (!dependencyMatch) {
137
+ continue;
138
+ }
139
+ addDependencyNames(lockDependencies, currentName, [dependencyMatch[1]]);
103
140
  }
104
- return toLockPackageRecord(lockPackages);
141
+ addDependencyNames(lockDependencies, "__root__", Object.keys(rootDependencies));
142
+ return {
143
+ lockPackages: toLockPackageRecord(lockPackages),
144
+ lockDependencies: toDependencyRecord(lockDependencies)
145
+ };
105
146
  }
106
- function parseYarnLockfile(content) {
147
+ function parseYarnLockfile(content, rootDependencies) {
107
148
  const lockPackages = new Map();
149
+ const lockDependencies = new Map();
150
+ const lines = content.split(/\r?\n/);
108
151
  let currentNames = [];
109
- for (const line of content.split(/\r?\n/)) {
152
+ let currentVersion = null;
153
+ let inDependenciesBlock = false;
154
+ for (const line of lines) {
110
155
  if (line.trim().length === 0 || line.startsWith("#")) {
111
156
  continue;
112
157
  }
113
158
  if (!line.startsWith(" ") && line.endsWith(":")) {
114
159
  currentNames = extractYarnEntryNames(line.slice(0, -1));
160
+ currentVersion = null;
161
+ inDependenciesBlock = false;
115
162
  continue;
116
163
  }
117
164
  const versionMatch = line.match(/^\s+version\s+"?([^"\s]+)"?\s*$/);
118
- if (!versionMatch) {
165
+ if (versionMatch) {
166
+ currentVersion = versionMatch[1];
167
+ for (const name of currentNames) {
168
+ addLockPackage(lockPackages, name, `yarn:${name}@${currentVersion}`, currentVersion);
169
+ }
170
+ continue;
171
+ }
172
+ if (/^\s{2}dependencies:\s*$/.test(line)) {
173
+ inDependenciesBlock = true;
174
+ continue;
175
+ }
176
+ if (/^\s{2}\S/.test(line) && !/^\s{2}dependencies:\s*$/.test(line)) {
177
+ inDependenciesBlock = false;
178
+ }
179
+ if (!inDependenciesBlock) {
180
+ continue;
181
+ }
182
+ const dependencyMatch = line.match(/^\s{4}((?:@[^/]+\/)?[^"\s]+)\s+/);
183
+ if (!dependencyMatch) {
119
184
  continue;
120
185
  }
121
186
  for (const name of currentNames) {
122
- addLockPackage(lockPackages, name, `yarn-lock:${name}@${versionMatch[1]}`, versionMatch[1]);
187
+ addDependencyNames(lockDependencies, name, [dependencyMatch[1]]);
123
188
  }
124
189
  }
125
- return toLockPackageRecord(lockPackages);
190
+ addDependencyNames(lockDependencies, "__root__", Object.keys(rootDependencies));
191
+ return {
192
+ lockPackages: toLockPackageRecord(lockPackages),
193
+ lockDependencies: toDependencyRecord(lockDependencies)
194
+ };
195
+ }
196
+ function extractPackageName(packagePath) {
197
+ if (!packagePath) {
198
+ return null;
199
+ }
200
+ const match = packagePath.match(/(?:^|\/)node_modules\/(.+)$/);
201
+ if (!match) {
202
+ return null;
203
+ }
204
+ return match[1];
126
205
  }
127
206
  function extractYarnEntryNames(entry) {
128
207
  const names = new Set();
@@ -149,9 +228,25 @@ function addLockPackage(lockPackages, name, packagePath, version) {
149
228
  instances.set(packagePath, { path: packagePath, version });
150
229
  lockPackages.set(name, instances);
151
230
  }
231
+ function addDependencyNames(lockDependencies, name, dependencies) {
232
+ if (dependencies.length === 0) {
233
+ return;
234
+ }
235
+ const entry = lockDependencies.get(name) ?? new Set();
236
+ for (const dependency of dependencies) {
237
+ entry.add(dependency);
238
+ }
239
+ lockDependencies.set(name, entry);
240
+ }
152
241
  function toLockPackageRecord(lockPackages) {
153
242
  return Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
154
243
  name,
155
244
  Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
156
245
  ]));
157
246
  }
247
+ function toDependencyRecord(lockDependencies) {
248
+ return Object.fromEntries(Array.from(lockDependencies.entries()).map(([name, dependencies]) => [
249
+ name,
250
+ Array.from(dependencies).sort((left, right) => left.localeCompare(right))
251
+ ]));
252
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { analyzeProject } from "./core/analyzer.js";
2
- export type { AnalysisOptions, AnalysisFocus, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
2
+ export type { AnalysisOptions, AnalysisFocus, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, RiskTransitiveDependency, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
3
3
  export { OUTPUT_VERSION } from "./core/analyzer.js";
4
4
  export { PluginManager } from "./core/plugin-manager.js";
5
5
  export type { DepBrainPlugin, PluginDiagnostic, ProjectContext } from "./core/plugin-manager.js";
@@ -28,11 +28,11 @@ export function renderConsoleReport(result) {
28
28
  ? `${item.name} (${item.section}) [${item.package}]`
29
29
  : `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
30
30
  appendSection(lines, "Outdated dependencies", result.outdated.map((item) => formatEntry(item.package
31
- ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
32
- : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
31
+ ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]${formatOutdatedAdviceSuffix(item)}`
32
+ : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]${formatOutdatedAdviceSuffix(item)}`, 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}`;
@@ -64,3 +71,12 @@ function formatEntry(label, confidence, explanation, recommendation) {
64
71
  : "";
65
72
  return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
66
73
  }
74
+ function formatOutdatedAdviceSuffix(item) {
75
+ if (!item.advice.recommendedTarget) {
76
+ return "";
77
+ }
78
+ const steps = item.advice.intermediateSteps.length > 1
79
+ ? ` steps ${item.advice.intermediateSteps.join(" -> ")}`
80
+ : "";
81
+ return ` [advice ${item.advice.risk.toUpperCase()} target ${item.advice.recommendedTarget}${steps}]`;
82
+ }
@@ -1,6 +1,17 @@
1
1
  export function renderDashboardReport(result) {
2
2
  const topIssues = result.topIssues.map(renderTopIssue).join("");
3
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
+ const upgradeAdvice = result.outdated
10
+ .filter((item) => item.advice.risk !== "low" || item.updateType === "major")
11
+ .sort((left, right) => compareAdviceRisk(right.advice.risk, left.advice.risk) ||
12
+ left.name.localeCompare(right.name))
13
+ .map(renderUpgradeAdvice)
14
+ .join("");
4
15
  return [
5
16
  "<!doctype html>",
6
17
  '<html lang="en">',
@@ -14,9 +25,11 @@ export function renderDashboardReport(result) {
14
25
  "header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}",
15
26
  "h1{font-size:28px;margin:0 0 8px}",
16
27
  "h2{font-size:18px;margin:0 0 12px}",
28
+ "h3{font-size:15px;margin:0 0 8px}",
17
29
  ".muted{color:#637083;font-size:13px}",
18
30
  ".score{font-size:48px;font-weight:700}",
19
31
  ".grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:20px}",
32
+ ".split{display:grid;grid-template-columns:1.2fr .8fr;gap:16px;margin-top:16px}",
20
33
  ".panel{background:#fff;border:1px solid #dce3ee;border-radius:8px;padding:16px}",
21
34
  ".metric{font-size:28px;font-weight:700;margin-top:4px}",
22
35
  ".pass{color:#167a43}.fail{color:#b42318}",
@@ -24,7 +37,9 @@ export function renderDashboardReport(result) {
24
37
  "li{margin:8px 0}",
25
38
  ".issue{margin-bottom:10px}",
26
39
  ".kind{font-size:12px;text-transform:uppercase;color:#637083}",
27
- "@media(max-width:760px){main{padding:20px}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}header{display:block}.score{font-size:40px}}",
40
+ ".hotspot{border-top:1px solid #e7ecf4;padding-top:12px;margin-top:12px}",
41
+ ".path{font-family:Consolas,monospace;font-size:12px;background:#f3f6fb;border-radius:6px;padding:6px 8px;margin:6px 0}",
42
+ "@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}}",
28
43
  "</style>",
29
44
  "</head>",
30
45
  "<body>",
@@ -42,20 +57,42 @@ export function renderDashboardReport(result) {
42
57
  renderMetric("Outdated", result.outdated.length),
43
58
  renderMetric("Risks", result.risks.length),
44
59
  "</section>",
45
- '<section class="panel">',
60
+ '<section class="split">',
61
+ '<div class="panel">',
46
62
  "<h2>Policy</h2>",
47
63
  `<p class="${result.policy.passed ? "pass" : "fail"}">${result.policy.passed ? "Passed" : "Failed"}</p>`,
48
64
  result.policy.reasons.length > 0
49
65
  ? `<ul>${result.policy.reasons.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`
50
66
  : '<p class="muted">No policy failures.</p>',
67
+ "</div>",
68
+ '<div class="panel">',
69
+ "<h2>Risk Snapshot</h2>",
70
+ `<div class="muted">${result.risks.filter((item) => item.riskyTransitiveDeps.length > 0).length} direct dependencies carry transitive risk.</div>`,
71
+ `<div class="metric">${result.risks.reduce((total, item) => total + item.transitiveRiskScore, 0)}</div>`,
72
+ '<div class="muted">Total transitive risk score</div>',
73
+ "</div>",
51
74
  "</section>",
52
- '<section class="panel">',
75
+ '<section class="split">',
76
+ '<div class="panel">',
53
77
  "<h2>Top Issues</h2>",
54
78
  topIssues.length > 0 ? `<ol>${topIssues}</ol>` : '<p class="muted">No actionable issues found.</p>',
55
- "</section>",
56
- '<section class="panel">',
79
+ "</div>",
80
+ '<div class="panel">',
57
81
  "<h2>Suggestions</h2>",
58
82
  suggestions.length > 0 ? `<ul>${suggestions}</ul>` : '<p class="muted">No suggestions.</p>',
83
+ "</div>",
84
+ "</section>",
85
+ '<section class="panel">',
86
+ "<h2>Upgrade Priorities</h2>",
87
+ upgradeAdvice.length > 0
88
+ ? `<ul>${upgradeAdvice}</ul>`
89
+ : '<p class="muted">No high-risk upgrades found.</p>',
90
+ "</section>",
91
+ '<section class="panel">',
92
+ "<h2>Transitive Risk Hotspots</h2>",
93
+ transitiveHotspots.length > 0
94
+ ? transitiveHotspots
95
+ : '<p class="muted">No transitive risk hotspots found.</p>',
59
96
  "</section>",
60
97
  "</main>",
61
98
  "</body>",
@@ -74,6 +111,38 @@ function renderTopIssue(item) {
74
111
  "</li>"
75
112
  ].join("");
76
113
  }
114
+ function renderTransitiveHotspot(item) {
115
+ return [
116
+ '<div class="hotspot">',
117
+ `<h3>${escapeHtml(item.name)} <span class="muted">score ${item.transitiveRiskScore}</span></h3>`,
118
+ `<div class="muted">${item.riskFactors.transitiveDependencyCount} transitive dependencies, ${item.riskyTransitiveDeps.length} risky transitive dependencies</div>`,
119
+ "<ul>",
120
+ item.riskyTransitiveDeps
121
+ .slice(0, 4)
122
+ .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>`)
123
+ .join(""),
124
+ "</ul>",
125
+ "</div>"
126
+ ].join("");
127
+ }
128
+ function renderUpgradeAdvice(item) {
129
+ return [
130
+ "<li>",
131
+ `<strong>${escapeHtml(item.name)}</strong> <span class="muted">[${escapeHtml(item.advice.risk.toUpperCase())}]</span>`,
132
+ `<div>${escapeHtml(item.current)} -> ${escapeHtml(item.latest)} | target ${escapeHtml(item.advice.recommendedTarget)}</div>`,
133
+ item.advice.intermediateSteps.length > 1
134
+ ? `<div class="path">${escapeHtml(item.advice.intermediateSteps.join(" -> "))}</div>`
135
+ : "",
136
+ item.advice.releaseNotes[0]
137
+ ? `<div class="muted">${escapeHtml(item.advice.releaseNotes[0])}</div>`
138
+ : "",
139
+ "</li>"
140
+ ].join("");
141
+ }
142
+ function compareAdviceRisk(left, right) {
143
+ const rank = { high: 3, medium: 2, low: 1 };
144
+ return rank[left] - rank[right];
145
+ }
77
146
  function escapeHtml(value) {
78
147
  return value
79
148
  .replace(/&/g, "&amp;")
@@ -32,11 +32,11 @@ export function renderMarkdownReport(result) {
32
32
  ? `${item.name} (${item.section}) [${item.package}]`
33
33
  : `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
34
34
  appendSection(lines, "Outdated dependencies", result.outdated.map((item) => formatEntry(item.package
35
- ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
36
- : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
35
+ ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]${formatOutdatedAdviceSuffix(item)}`
36
+ : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]${formatOutdatedAdviceSuffix(item)}`, 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;
@@ -64,3 +71,12 @@ function formatEntry(label, confidence, explanation, recommendation) {
64
71
  : "";
65
72
  return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
66
73
  }
74
+ function formatOutdatedAdviceSuffix(item) {
75
+ if (!item.advice.recommendedTarget) {
76
+ return "";
77
+ }
78
+ const steps = item.advice.intermediateSteps.length > 1
79
+ ? ` steps ${item.advice.intermediateSteps.join(" -> ")}`
80
+ : "";
81
+ return ` [advice ${item.advice.risk.toUpperCase()} target ${item.advice.recommendedTarget}${steps}]`;
82
+ }
@@ -1,11 +1,13 @@
1
1
  export interface PackageMetadata {
2
2
  latestVersion: string | null;
3
3
  repository: string | null;
4
+ homepage: string | null;
4
5
  downloads: number | null;
5
6
  daysSincePublish: number | null;
6
7
  maintainersCount: number | null;
7
8
  versionCount: number | null;
8
9
  recentReleaseCount: number | null;
10
+ versions: string[];
9
11
  }
10
12
  export declare function getLatestVersion(name: string): Promise<string | null>;
11
13
  export declare function getPackageMetadata(name: string): Promise<PackageMetadata | null>;
@@ -49,11 +49,13 @@ async function fetchPackageMetadata(name) {
49
49
  return {
50
50
  latestVersion,
51
51
  repository,
52
+ homepage: typeof packageJson.homepage === "string" ? packageJson.homepage : null,
52
53
  downloads: downloadsJson.downloads ?? null,
53
54
  daysSincePublish,
54
55
  maintainersCount,
55
56
  versionCount,
56
- recentReleaseCount
57
+ recentReleaseCount,
58
+ versions: Object.keys(packageJson.versions ?? {})
57
59
  };
58
60
  }
59
61
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "CLI and library for explainable dependency intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",