dep-brain 1.4.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 1.5.0
6
+
7
+ - Added structured upgrade advisor data under `outdated[].advice`.
8
+ - Added `--advise` report mode for upgrade guidance output.
9
+ - Added stepped major-upgrade recommendations, release-note links, and breaking-change signals.
10
+ - Updated dashboard, console, and markdown outputs with upgrade-priority guidance.
11
+ - Bumped analysis output contract to `1.6` and added regression coverage for update advice.
12
+
5
13
  ## 1.4.0
6
14
 
7
15
  - Added lockfile dependency-edge parsing so transitive relationships are available from npm, pnpm, and yarn lockfiles.
package/README.md CHANGED
@@ -25,6 +25,7 @@
25
25
  - Score package trust using supply-chain metadata
26
26
  - Generate a simple project health score
27
27
  - Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
28
+ - Output upgrade-advice reports via `--advise`
28
29
  - Gate CI with score and finding policies
29
30
  - Compare new findings against a baseline report
30
31
 
@@ -51,6 +52,7 @@ The long-term goal is not just to list problems, but to answer:
51
52
  - Markdown output via `--md`
52
53
  - SARIF output via `--sarif`
53
54
  - Static HTML dashboard via `--dashboard`
55
+ - Upgrade advisor output via `--advise`
54
56
  - Ranked top issues via `--top`
55
57
  - Baseline mode via `--baseline`
56
58
  - Focused analysis via `--focus`
@@ -70,6 +72,7 @@ npx dep-brain analyze
70
72
  npx dep-brain analyze --json
71
73
  npx dep-brain analyze --md
72
74
  npx dep-brain analyze --top
75
+ npx dep-brain analyze --advise
73
76
  npx dep-brain analyze ./path-to-project
74
77
  npx dep-brain analyze --config depbrain.config.json
75
78
  npx dep-brain analyze --min-score 90 --fail-on-risks
@@ -182,6 +185,14 @@ dep-brain analyze --top
182
185
 
183
186
  Shows the highest-priority actionable findings first, including confidence and next-step guidance.
184
187
 
188
+ ## Upgrade Advice Output
189
+
190
+ ```bash
191
+ dep-brain analyze --advise
192
+ ```
193
+
194
+ Shows recommended upgrade targets, stepped major-version paths, and release-note links when available.
195
+
185
196
  ## Dashboard Output
186
197
 
187
198
  ```bash
@@ -204,6 +215,8 @@ Writes a static HTML dashboard. Default path comes from `dashboard.outputPath`.
204
215
 
205
216
  Built-in `license` plugin adds license counts under `extensions.license`. Failed plugin loads and hook errors are reported under `extensions.depBrain.plugins`.
206
217
 
218
+ Outdated results now include `advice` with `risk`, `recommendedTarget`, `intermediateSteps`, `releaseNotes`, and upgrade signals such as `semver_major`.
219
+
207
220
  ## Report From JSON
208
221
 
209
222
  ```bash
@@ -17,6 +17,23 @@
17
17
  "reasons": { "type": "array", "items": { "type": "string" } }
18
18
  }
19
19
  },
20
+ "outdatedAdvice": {
21
+ "type": "object",
22
+ "required": ["risk", "recommendedTarget", "latestEvaluatedVersion", "intermediateSteps", "releaseNotes", "signals", "currentRange"],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "risk": { "type": "string", "enum": ["low", "medium", "high"] },
26
+ "recommendedTarget": { "type": "string" },
27
+ "latestEvaluatedVersion": { "type": "string" },
28
+ "intermediateSteps": { "type": "array", "items": { "type": "string" } },
29
+ "releaseNotes": { "type": "array", "items": { "type": "string" } },
30
+ "signals": {
31
+ "type": "array",
32
+ "items": { "type": "string", "enum": ["semver_major", "breaking_keyword", "missing_changelog"] }
33
+ },
34
+ "currentRange": { "type": "string" }
35
+ }
36
+ },
20
37
  "riskFactors": {
21
38
  "type": "object",
22
39
  "required": ["daysSincePublish", "downloads", "maintainersCount", "versionCount", "recentReleaseCount", "hasRepository", "dependencyType", "transitiveDependencyCount", "riskyTransitiveCount"],
@@ -154,7 +171,7 @@
154
171
  "type": "array",
155
172
  "items": {
156
173
  "type": "object",
157
- "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation", "recommendation"],
174
+ "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation", "advice", "recommendation"],
158
175
  "additionalProperties": false,
159
176
  "properties": {
160
177
  "name": { "type": "string" },
@@ -165,6 +182,7 @@
165
182
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
166
183
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
167
184
  "explanation": { "type": "array", "items": { "type": "string" } },
185
+ "advice": { "$ref": "#/properties/outdatedAdvice" },
168
186
  "recommendation": { "$ref": "#/properties/recommendation" }
169
187
  }
170
188
  }
@@ -1,8 +1,11 @@
1
1
  import type { OutdatedDependency } from "../core/analyzer.js";
2
2
  import type { DependencyGraph } from "../core/graph-builder.js";
3
3
  import type { CheckResult } from "../core/types.js";
4
+ import { type PackageMetadata } from "../utils/npm-api.js";
4
5
  export interface OutdatedOptions {
5
6
  resolveLatestVersion?: (name: string) => Promise<string | null>;
7
+ resolvePackageMetadata?: (name: string) => Promise<PackageMetadata | null>;
8
+ resolveReleaseNotesText?: (url: string) => Promise<string | null>;
6
9
  }
7
10
  export declare function findOutdatedDependencies(graph: DependencyGraph, options?: OutdatedOptions): Promise<OutdatedDependency[]>;
8
- export declare function runOutdatedCheck(graph: DependencyGraph): Promise<CheckResult>;
11
+ export declare function runOutdatedCheck(graph: DependencyGraph, options?: OutdatedOptions): Promise<CheckResult>;
@@ -1,32 +1,36 @@
1
- import { getLatestVersion } from "../utils/npm-api.js";
1
+ import { getLatestVersion, getPackageMetadata } from "../utils/npm-api.js";
2
2
  export async function findOutdatedDependencies(graph, options = {}) {
3
- const resolveLatestVersion = options.resolveLatestVersion ?? getLatestVersion;
3
+ const resolveLatestVersion = options.resolveLatestVersion;
4
+ const resolvePackageMetadata = options.resolvePackageMetadata;
4
5
  const combined = {
5
6
  ...graph.dependencies,
6
7
  ...graph.devDependencies
7
8
  };
8
9
  const results = await mapWithConcurrency(Object.entries(combined), 8, async ([name, current]) => {
9
10
  const normalized = normalizeVersion(current);
10
- const latest = await resolveLatestVersion(name);
11
+ const metadata = resolvePackageMetadata
12
+ ? await resolvePackageMetadata(name)
13
+ : resolveLatestVersion
14
+ ? null
15
+ : await getPackageMetadata(name);
16
+ const latest = resolveLatestVersion
17
+ ? await resolveLatestVersion(name)
18
+ : metadata?.latestVersion ?? await getLatestVersion(name);
11
19
  if (!latest || latest === normalized) {
12
20
  return null;
13
21
  }
14
22
  const updateType = classifyUpdateType(normalized, latest);
23
+ const advice = await buildAdvice(name, current, normalized, latest, updateType, metadata, options.resolveReleaseNotesText);
15
24
  return {
16
25
  name,
17
26
  current,
18
27
  latest,
19
28
  updateType,
20
- confidence: 0.97,
21
- reasonCodes: [
22
- "latest_registry_version_newer",
23
- `update_type_${updateType}`
24
- ],
25
- explanation: [
26
- "The npm registry reports a newer published version than the one declared in this project.",
27
- `The change is classified as a ${updateType} update.`
28
- ],
29
- recommendation: buildOutdatedRecommendation(updateType)
29
+ confidence: advice.risk === "high" ? 0.98 : 0.97,
30
+ reasonCodes: buildReasonCodes(updateType, advice),
31
+ explanation: buildExplanation(updateType, advice),
32
+ advice,
33
+ recommendation: buildOutdatedRecommendation(updateType, advice)
30
34
  };
31
35
  });
32
36
  return results
@@ -47,32 +51,43 @@ async function mapWithConcurrency(items, limit, mapper) {
47
51
  await Promise.all(workers);
48
52
  return results;
49
53
  }
50
- function buildOutdatedRecommendation(updateType) {
54
+ function buildOutdatedRecommendation(updateType, advice) {
51
55
  return {
52
56
  action: "upgrade",
53
- priority: updateType === "major" ? "high" : updateType === "minor" ? "medium" : "low",
54
- safety: updateType === "patch" ? "safe" : updateType === "minor" ? "caution" : "unknown",
55
- summary: updateType === "major"
56
- ? "New major version available; review breaking changes before upgrading."
57
- : updateType === "minor"
58
- ? "New minor version available; review release notes before upgrading."
59
- : updateType === "patch"
60
- ? "Routine patch update available."
61
- : "Newer version available; review upgrade impact.",
57
+ priority: advice.risk === "high"
58
+ ? "high"
59
+ : updateType === "major"
60
+ ? "high"
61
+ : updateType === "minor"
62
+ ? "medium"
63
+ : "low",
64
+ safety: advice.risk === "high"
65
+ ? "unknown"
66
+ : updateType === "patch"
67
+ ? "safe"
68
+ : updateType === "minor"
69
+ ? "caution"
70
+ : "unknown",
71
+ summary: advice.risk === "high"
72
+ ? `Upgrade in steps toward ${advice.recommendedTarget}; review breaking signals first.`
73
+ : advice.risk === "medium"
74
+ ? `Upgrade toward ${advice.recommendedTarget} after reviewing release notes.`
75
+ : `Upgrade toward ${advice.recommendedTarget}.`,
62
76
  reasons: [
63
- "A newer published version is available in the registry."
77
+ `Latest registry version is ${advice.latestEvaluatedVersion}.`,
78
+ ...advice.signals.map((signal) => formatAdviceSignal(signal))
64
79
  ]
65
80
  };
66
81
  }
67
- export async function runOutdatedCheck(graph) {
68
- const outdated = await findOutdatedDependencies(graph);
82
+ export async function runOutdatedCheck(graph, options = {}) {
83
+ const outdated = await findOutdatedDependencies(graph, options);
69
84
  return {
70
85
  name: "outdated",
71
86
  summary: `${outdated.length} outdated dependencies found`,
72
87
  issues: outdated.map((item) => ({
73
88
  id: `outdated:${item.name}`,
74
89
  message: `${item.name} ${item.current} -> ${item.latest}`,
75
- severity: item.updateType === "major" ? "critical" : "warning",
90
+ severity: item.advice.risk === "high" || item.updateType === "major" ? "critical" : "warning",
76
91
  confidence: item.confidence,
77
92
  reasonCodes: item.reasonCodes,
78
93
  explanation: item.explanation,
@@ -80,11 +95,90 @@ export async function runOutdatedCheck(graph) {
80
95
  name: item.name,
81
96
  current: item.current,
82
97
  latest: item.latest,
83
- updateType: item.updateType
98
+ updateType: item.updateType,
99
+ advice: item.advice
84
100
  }
85
101
  }))
86
102
  };
87
103
  }
104
+ async function buildAdvice(name, currentRange, normalizedCurrent, latest, updateType, metadata, resolveReleaseNotesText) {
105
+ const versions = (metadata?.versions ?? []).filter((version) => parseVersion(version) !== null);
106
+ const repositoryUrl = normalizeRepositoryUrl(metadata?.repository ?? null);
107
+ const releaseNotes = buildReleaseNoteUrls(repositoryUrl, latest, versions);
108
+ const currentParsed = parseVersion(normalizedCurrent);
109
+ const latestParsed = parseVersion(latest);
110
+ const signals = [];
111
+ let recommendedTarget = latest;
112
+ let intermediateSteps = [];
113
+ if (updateType === "major" && currentParsed && latestParsed) {
114
+ const stableTarget = findHighestVersionInMajor(versions, currentParsed[0]);
115
+ if (stableTarget && stableTarget !== normalizedCurrent && stableTarget !== latest) {
116
+ recommendedTarget = stableTarget;
117
+ intermediateSteps = [stableTarget, latest];
118
+ signals.push("semver_major");
119
+ }
120
+ else {
121
+ intermediateSteps = [latest];
122
+ signals.push("semver_major");
123
+ }
124
+ }
125
+ else if (updateType !== "unknown") {
126
+ intermediateSteps = [latest];
127
+ }
128
+ let hasBreakingKeyword = false;
129
+ if (resolveReleaseNotesText && releaseNotes.length > 0) {
130
+ const notesText = await resolveReleaseNotesText(releaseNotes[0]);
131
+ if (notesText && /\bBREAKING\b/i.test(notesText)) {
132
+ hasBreakingKeyword = true;
133
+ signals.push("breaking_keyword");
134
+ }
135
+ }
136
+ if (releaseNotes.length === 0) {
137
+ signals.push("missing_changelog");
138
+ }
139
+ const risk = hasBreakingKeyword || updateType === "major"
140
+ ? "high"
141
+ : updateType === "minor"
142
+ ? "medium"
143
+ : "low";
144
+ return {
145
+ risk,
146
+ recommendedTarget,
147
+ latestEvaluatedVersion: latest,
148
+ intermediateSteps: intermediateSteps.length > 0 ? intermediateSteps : [latest],
149
+ releaseNotes,
150
+ signals: dedupeSignals(signals),
151
+ currentRange
152
+ };
153
+ }
154
+ function buildReasonCodes(updateType, advice) {
155
+ const codes = [
156
+ "latest_registry_version_newer",
157
+ `update_type_${updateType}`,
158
+ `advice_risk_${advice.risk}`
159
+ ];
160
+ for (const signal of advice.signals) {
161
+ codes.push(`advice_signal_${signal}`);
162
+ }
163
+ return codes;
164
+ }
165
+ function buildExplanation(updateType, advice) {
166
+ const explanation = [
167
+ "The npm registry reports a newer published version than the one declared in this project.",
168
+ `The change is classified as a ${updateType} update.`,
169
+ `Recommended target is ${advice.recommendedTarget}.`
170
+ ];
171
+ if (advice.intermediateSteps.length > 1) {
172
+ explanation.push(`Suggested upgrade path: ${advice.intermediateSteps.join(" -> ")}.`);
173
+ }
174
+ if (advice.releaseNotes.length > 0) {
175
+ explanation.push(`Release notes available at ${advice.releaseNotes[0]}.`);
176
+ }
177
+ for (const signal of advice.signals) {
178
+ explanation.push(formatAdviceSignal(signal));
179
+ }
180
+ return explanation;
181
+ }
88
182
  function normalizeVersion(versionRange) {
89
183
  return versionRange.trim().replace(/^[~^><=\s]+/, "");
90
184
  }
@@ -112,3 +206,61 @@ function parseVersion(version) {
112
206
  }
113
207
  return [Number(match[1]), Number(match[2]), Number(match[3])];
114
208
  }
209
+ function normalizeRepositoryUrl(value) {
210
+ if (!value) {
211
+ return null;
212
+ }
213
+ return value
214
+ .replace(/^git\+/, "")
215
+ .replace(/^git:\/\/github\.com\//, "https://github.com/")
216
+ .replace(/^git@github\.com:/, "https://github.com/")
217
+ .replace(/\.git$/, "");
218
+ }
219
+ function buildReleaseNoteUrls(repositoryUrl, latestVersion, versions) {
220
+ if (!repositoryUrl) {
221
+ return [];
222
+ }
223
+ const urls = [];
224
+ if (/github\.com\//.test(repositoryUrl)) {
225
+ urls.push(`${repositoryUrl}/releases/tag/v${latestVersion}`);
226
+ urls.push(`${repositoryUrl}/releases`);
227
+ }
228
+ else {
229
+ urls.push(repositoryUrl);
230
+ }
231
+ if (versions.length === 0 && urls.length === 0) {
232
+ return [];
233
+ }
234
+ return Array.from(new Set(urls));
235
+ }
236
+ function findHighestVersionInMajor(versions, major) {
237
+ const matching = versions
238
+ .map((version) => ({ version, parsed: parseVersion(version) }))
239
+ .filter((entry) => entry.parsed !== null && entry.parsed[0] === major)
240
+ .sort((left, right) => compareParsedVersions(right.parsed, left.parsed));
241
+ return matching[0]?.version ?? null;
242
+ }
243
+ function compareParsedVersions(left, right) {
244
+ if (left[0] !== right[0]) {
245
+ return left[0] - right[0];
246
+ }
247
+ if (left[1] !== right[1]) {
248
+ return left[1] - right[1];
249
+ }
250
+ return left[2] - right[2];
251
+ }
252
+ function dedupeSignals(values) {
253
+ return Array.from(new Set(values));
254
+ }
255
+ function formatAdviceSignal(signal) {
256
+ switch (signal) {
257
+ case "semver_major":
258
+ return "Major version gap detected.";
259
+ case "breaking_keyword":
260
+ return "Release notes include BREAKING markers.";
261
+ case "missing_changelog":
262
+ return "Release notes were not discovered automatically.";
263
+ default:
264
+ return signal;
265
+ }
266
+ }
package/dist/cli.js CHANGED
@@ -56,13 +56,15 @@ async function main() {
56
56
  const reportData = JSON.parse(raw);
57
57
  const output = flags.has("--top")
58
58
  ? renderTopIssuesReport(reportData)
59
- : flags.has("--json")
60
- ? JSON.stringify(reportData, null, 2)
61
- : flags.has("--sarif")
62
- ? renderSarifReport(reportData)
63
- : flags.has("--dashboard")
64
- ? renderDashboardReport(reportData)
65
- : renderMarkdownReport(reportData);
59
+ : flags.has("--advise")
60
+ ? renderUpgradeAdviceReport(reportData)
61
+ : flags.has("--json")
62
+ ? JSON.stringify(reportData, null, 2)
63
+ : flags.has("--sarif")
64
+ ? renderSarifReport(reportData)
65
+ : flags.has("--dashboard")
66
+ ? renderDashboardReport(reportData)
67
+ : renderMarkdownReport(reportData);
66
68
  await writeOutput(output, optionValues.get("--out"));
67
69
  return;
68
70
  }
@@ -141,6 +143,9 @@ async function main() {
141
143
  else if (flags.has("--top")) {
142
144
  output = renderTopIssuesReport(result);
143
145
  }
146
+ else if (flags.has("--advise")) {
147
+ output = renderUpgradeAdviceReport(result);
148
+ }
144
149
  else if (flags.has("--md")) {
145
150
  output = renderMarkdownReport(result);
146
151
  }
@@ -216,7 +221,7 @@ function printHelp() {
216
221
  console.log("");
217
222
  console.log("Usage:");
218
223
  console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
219
- console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--dashboard] [--out path]");
224
+ console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--advise] [--dashboard] [--out path]");
220
225
  console.log(" dep-brain config [path] [--config path]");
221
226
  console.log(" dep-brain init [--out depbrain.config.json]");
222
227
  console.log(" dep-brain help");
@@ -227,6 +232,7 @@ function printHelp() {
227
232
  console.log(" --md Output Markdown report");
228
233
  console.log(" --sarif Output SARIF format for Code Scanning");
229
234
  console.log(" --top Output the ranked top issues only");
235
+ console.log(" --advise Output upgrade advice for outdated dependencies");
230
236
  console.log(" --dashboard Write an HTML dashboard");
231
237
  console.log(" --dashboard-out <path> Write dashboard HTML to a custom path");
232
238
  console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
@@ -336,3 +342,29 @@ function renderTopIssuesReport(result) {
336
342
  }
337
343
  return lines.join("\n");
338
344
  }
345
+ function renderUpgradeAdviceReport(result) {
346
+ const lines = [];
347
+ lines.push("Upgrade Advice");
348
+ lines.push("");
349
+ if (!Array.isArray(result.outdated) || result.outdated.length === 0) {
350
+ lines.push("No outdated dependencies found.");
351
+ return lines.join("\n");
352
+ }
353
+ const sorted = [...result.outdated].sort((left, right) => compareAdviceRisk(right.advice.risk, left.advice.risk) ||
354
+ left.name.localeCompare(right.name));
355
+ for (const item of sorted) {
356
+ lines.push(`- ${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.advice.risk.toUpperCase()}]`);
357
+ lines.push(` Target: ${item.advice.recommendedTarget}`);
358
+ if (item.advice.intermediateSteps.length > 1) {
359
+ lines.push(` Steps: ${item.advice.intermediateSteps.join(" -> ")}`);
360
+ }
361
+ if (item.advice.releaseNotes[0]) {
362
+ lines.push(` Notes: ${item.advice.releaseNotes[0]}`);
363
+ }
364
+ }
365
+ return lines.join("\n");
366
+ }
367
+ function compareAdviceRisk(left, right) {
368
+ const rank = { high: 3, medium: 2, low: 1 };
369
+ return rank[left] - rank[right];
370
+ }
@@ -83,8 +83,18 @@ export interface OutdatedDependency {
83
83
  confidence: number;
84
84
  reasonCodes: string[];
85
85
  explanation: string[];
86
+ advice: OutdatedDependencyAdvice;
86
87
  recommendation: Recommendation;
87
88
  }
89
+ export interface OutdatedDependencyAdvice {
90
+ risk: "low" | "medium" | "high";
91
+ recommendedTarget: string;
92
+ latestEvaluatedVersion: string;
93
+ intermediateSteps: string[];
94
+ releaseNotes: string[];
95
+ signals: Array<"semver_major" | "breaking_keyword" | "missing_changelog">;
96
+ currentRange: string;
97
+ }
88
98
  export interface RiskDependency {
89
99
  name: string;
90
100
  reasons: string[];
@@ -144,7 +154,7 @@ export interface PackageAnalysisResult {
144
154
  topIssues: TopIssue[];
145
155
  extensions: Record<string, unknown>;
146
156
  }
147
- export declare const OUTPUT_VERSION = "1.5";
157
+ export declare const OUTPUT_VERSION = "1.6";
148
158
  export interface ScoreBreakdown {
149
159
  baseScore: number;
150
160
  duplicates: number;
@@ -9,7 +9,7 @@ import { buildDependencyGraph } from "./graph-builder.js";
9
9
  import { PluginManager } from "./plugin-manager.js";
10
10
  import { calculateHealthScore, calculateScoreDeductions } from "./scorer.js";
11
11
  import { buildAnalysisContext } from "./context.js";
12
- export const OUTPUT_VERSION = "1.5";
12
+ export const OUTPUT_VERSION = "1.6";
13
13
  export async function analyzeProject(options = {}) {
14
14
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
15
15
  const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
@@ -384,6 +384,7 @@ function mapOutdatedIssues(issues) {
384
384
  confidence: normalizeConfidence(issue.confidence),
385
385
  reasonCodes: normalizeStringArray(issue.reasonCodes),
386
386
  explanation: normalizeStringArray(issue.explanation),
387
+ advice: normalizeOutdatedAdvice(issue.meta?.advice, issue.meta?.current, issue.meta?.latest),
387
388
  recommendation: buildOutdatedRecommendation(issue)
388
389
  }));
389
390
  }
@@ -443,19 +444,30 @@ function buildOutdatedRecommendation(issue) {
443
444
  issue.meta?.updateType === "patch"
444
445
  ? issue.meta.updateType
445
446
  : "unknown";
446
- const priority = updateType === "major" ? "high" : updateType === "minor" ? "medium" : "low";
447
- const safety = updateType === "patch" ? "safe" : updateType === "minor" ? "caution" : "unknown";
447
+ const advice = normalizeOutdatedAdvice(issue.meta?.advice, issue.meta?.current, issue.meta?.latest);
448
+ const priority = advice.risk === "high"
449
+ ? "high"
450
+ : updateType === "major"
451
+ ? "high"
452
+ : updateType === "minor"
453
+ ? "medium"
454
+ : "low";
455
+ const safety = advice.risk === "high"
456
+ ? "unknown"
457
+ : updateType === "patch"
458
+ ? "safe"
459
+ : updateType === "minor"
460
+ ? "caution"
461
+ : "unknown";
448
462
  return {
449
463
  action: "upgrade",
450
464
  priority,
451
465
  safety,
452
- summary: updateType === "major"
453
- ? "New major version available; review breaking changes before upgrading."
454
- : updateType === "minor"
455
- ? "New minor version available; review release notes before upgrading."
456
- : updateType === "patch"
457
- ? "Routine patch update available."
458
- : "Newer version available; review upgrade impact.",
466
+ summary: advice.risk === "high"
467
+ ? `Upgrade in steps toward ${advice.recommendedTarget}; review breaking signals first.`
468
+ : advice.risk === "medium"
469
+ ? `Upgrade toward ${advice.recommendedTarget} after reviewing release notes.`
470
+ : `Upgrade toward ${advice.recommendedTarget}.`,
459
471
  reasons: normalizeStringArray(issue.explanation)
460
472
  };
461
473
  }
@@ -660,6 +672,45 @@ function normalizeRiskTransitiveDependencies(value) {
660
672
  })
661
673
  .filter((entry) => entry !== null);
662
674
  }
675
+ function normalizeOutdatedAdvice(value, current, latest) {
676
+ const fallbackLatest = typeof latest === "string" ? latest : "";
677
+ const fallbackCurrent = typeof current === "string" ? current : "";
678
+ if (!value || typeof value !== "object") {
679
+ return {
680
+ risk: "medium",
681
+ recommendedTarget: fallbackLatest,
682
+ latestEvaluatedVersion: fallbackLatest,
683
+ intermediateSteps: fallbackLatest ? [fallbackLatest] : [],
684
+ releaseNotes: [],
685
+ signals: [],
686
+ currentRange: fallbackCurrent
687
+ };
688
+ }
689
+ const advice = value;
690
+ return {
691
+ risk: advice.risk === "low" || advice.risk === "medium" || advice.risk === "high"
692
+ ? advice.risk
693
+ : "medium",
694
+ recommendedTarget: typeof advice.recommendedTarget === "string" ? advice.recommendedTarget : fallbackLatest,
695
+ latestEvaluatedVersion: typeof advice.latestEvaluatedVersion === "string"
696
+ ? advice.latestEvaluatedVersion
697
+ : fallbackLatest,
698
+ intermediateSteps: Array.isArray(advice.intermediateSteps)
699
+ ? advice.intermediateSteps.filter((step) => typeof step === "string")
700
+ : fallbackLatest
701
+ ? [fallbackLatest]
702
+ : [],
703
+ releaseNotes: Array.isArray(advice.releaseNotes)
704
+ ? advice.releaseNotes.filter((url) => typeof url === "string")
705
+ : [],
706
+ signals: Array.isArray(advice.signals)
707
+ ? advice.signals.filter((signal) => signal === "semver_major" ||
708
+ signal === "breaking_keyword" ||
709
+ signal === "missing_changelog")
710
+ : [],
711
+ currentRange: typeof advice.currentRange === "string" ? advice.currentRange : fallbackCurrent
712
+ };
713
+ }
663
714
  function normalizeWorkspaceUsage(value) {
664
715
  if (!Array.isArray(value)) {
665
716
  return [];
@@ -28,8 +28,8 @@ 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
34
  ? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`
35
35
  : `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
@@ -71,3 +71,12 @@ function formatEntry(label, confidence, explanation, recommendation) {
71
71
  : "";
72
72
  return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
73
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
+ }
@@ -6,6 +6,12 @@ export function renderDashboardReport(result) {
6
6
  .sort((left, right) => right.transitiveRiskScore - left.transitiveRiskScore)
7
7
  .map(renderTransitiveHotspot)
8
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("");
9
15
  return [
10
16
  "<!doctype html>",
11
17
  '<html lang="en">',
@@ -77,6 +83,12 @@ export function renderDashboardReport(result) {
77
83
  "</div>",
78
84
  "</section>",
79
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">',
80
92
  "<h2>Transitive Risk Hotspots</h2>",
81
93
  transitiveHotspots.length > 0
82
94
  ? transitiveHotspots
@@ -113,6 +125,24 @@ function renderTransitiveHotspot(item) {
113
125
  "</div>"
114
126
  ].join("");
115
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
+ }
116
146
  function escapeHtml(value) {
117
147
  return value
118
148
  .replace(/&/g, "&amp;")
@@ -32,8 +32,8 @@ 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
38
  ? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`
39
39
  : `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]${formatTransitiveRiskSuffix(item)}`, item.confidence, item.explanation, item.recommendation)));
@@ -71,3 +71,12 @@ function formatEntry(label, confidence, explanation, recommendation) {
71
71
  : "";
72
72
  return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
73
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.4.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",