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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
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
+
13
+ ## 1.4.0
14
+
15
+ - Added lockfile dependency-edge parsing so transitive relationships are available from npm, pnpm, and yarn lockfiles.
16
+ - Changed risk analysis from direct-package-only output to direct-owner risk summaries with `transitiveRiskScore` and `riskyTransitiveDeps`.
17
+ - Added transitive dependency counts and risky transitive counts to `riskFactors`.
18
+ - Updated console, markdown, and dashboard reports to highlight transitive risk hotspots.
19
+ - Bumped analysis output contract to `1.5` and added regression coverage for transitive risk propagation.
20
+
5
21
  ## 1.3.0
6
22
 
7
23
  - Added plugin diagnostics under `extensions.depBrain.plugins` for failed plugin loads and hook errors.
package/README.md CHANGED
@@ -21,9 +21,11 @@
21
21
  - Detect likely unused dependencies from source imports and scripts
22
22
  - Detect outdated packages
23
23
  - Highlight dependency risk signals
24
+ - Show which direct dependency introduces risky transitive packages
24
25
  - Score package trust using supply-chain metadata
25
26
  - Generate a simple project health score
26
27
  - Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
28
+ - Output upgrade-advice reports via `--advise`
27
29
  - Gate CI with score and finding policies
28
30
  - Compare new findings against a baseline report
29
31
 
@@ -39,6 +41,7 @@ The long-term goal is not just to list problems, but to answer:
39
41
  - Unused dependency detection with runtime vs dev-tool heuristics
40
42
  - Outdated dependency reporting with `major`, `minor`, and `patch` classification
41
43
  - Risk analysis based on npm package metadata
44
+ - Transitive risk ownership and path tracing for direct dependencies
42
45
  - Confidence scores, reason codes, explanations, and recommendations for findings
43
46
  - Config loading from `depbrain.config.json`
44
47
  - Ignore rules for noisy dependencies and checks
@@ -49,6 +52,7 @@ The long-term goal is not just to list problems, but to answer:
49
52
  - Markdown output via `--md`
50
53
  - SARIF output via `--sarif`
51
54
  - Static HTML dashboard via `--dashboard`
55
+ - Upgrade advisor output via `--advise`
52
56
  - Ranked top issues via `--top`
53
57
  - Baseline mode via `--baseline`
54
58
  - Focused analysis via `--focus`
@@ -68,6 +72,7 @@ npx dep-brain analyze
68
72
  npx dep-brain analyze --json
69
73
  npx dep-brain analyze --md
70
74
  npx dep-brain analyze --top
75
+ npx dep-brain analyze --advise
71
76
  npx dep-brain analyze ./path-to-project
72
77
  npx dep-brain analyze --config depbrain.config.json
73
78
  npx dep-brain analyze --min-score 90 --fail-on-risks
@@ -180,6 +185,14 @@ dep-brain analyze --top
180
185
 
181
186
  Shows the highest-priority actionable findings first, including confidence and next-step guidance.
182
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
+
183
196
  ## Dashboard Output
184
197
 
185
198
  ```bash
@@ -202,6 +215,8 @@ Writes a static HTML dashboard. Default path comes from `dashboard.outputPath`.
202
215
 
203
216
  Built-in `license` plugin adds license counts under `extensions.license`. Failed plugin loads and hook errors are reported under `extensions.depBrain.plugins`.
204
217
 
218
+ Outdated results now include `advice` with `risk`, `recommendedTarget`, `intermediateSteps`, `releaseNotes`, and upgrade signals such as `semver_major`.
219
+
205
220
  ## Report From JSON
206
221
 
207
222
  ```bash
@@ -379,7 +394,7 @@ src/
379
394
 
380
395
  The project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
381
396
 
382
- Risk findings now include a `trustScore` plus structured `riskFactors` such as publish recency, maintainer count, and repository presence.
397
+ Risk findings now include a `trustScore`, structured `riskFactors`, `transitiveRiskScore`, and `riskyTransitiveDeps` path traces so teams can see which direct package introduces supply-chain risk.
383
398
 
384
399
  ## Repository Notes
385
400
 
@@ -17,9 +17,26 @@
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
- "required": ["daysSincePublish", "downloads", "maintainersCount", "versionCount", "recentReleaseCount", "hasRepository", "dependencyType"],
39
+ "required": ["daysSincePublish", "downloads", "maintainersCount", "versionCount", "recentReleaseCount", "hasRepository", "dependencyType", "transitiveDependencyCount", "riskyTransitiveCount"],
23
40
  "additionalProperties": false,
24
41
  "properties": {
25
42
  "daysSincePublish": { "type": ["number", "null"] },
@@ -28,7 +45,21 @@
28
45
  "versionCount": { "type": ["number", "null"] },
29
46
  "recentReleaseCount": { "type": ["number", "null"] },
30
47
  "hasRepository": { "type": "boolean" },
31
- "dependencyType": { "type": "string", "enum": ["dependencies", "devDependencies", "unknown"] }
48
+ "dependencyType": { "type": "string", "enum": ["dependencies", "devDependencies", "unknown"] },
49
+ "transitiveDependencyCount": { "type": "number" },
50
+ "riskyTransitiveCount": { "type": "number" }
51
+ }
52
+ },
53
+ "riskTransitiveDependency": {
54
+ "type": "object",
55
+ "required": ["name", "trustScore", "confidence", "reasons", "introducedByPaths"],
56
+ "additionalProperties": false,
57
+ "properties": {
58
+ "name": { "type": "string" },
59
+ "trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
60
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
61
+ "reasons": { "type": "array", "items": { "type": "string" } },
62
+ "introducedByPaths": { "type": "array", "items": { "type": "string" } }
32
63
  }
33
64
  },
34
65
  "ownershipSummary": {
@@ -140,7 +171,7 @@
140
171
  "type": "array",
141
172
  "items": {
142
173
  "type": "object",
143
- "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation", "recommendation"],
174
+ "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation", "advice", "recommendation"],
144
175
  "additionalProperties": false,
145
176
  "properties": {
146
177
  "name": { "type": "string" },
@@ -151,6 +182,7 @@
151
182
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
152
183
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
153
184
  "explanation": { "type": "array", "items": { "type": "string" } },
185
+ "advice": { "$ref": "#/properties/outdatedAdvice" },
154
186
  "recommendation": { "$ref": "#/properties/recommendation" }
155
187
  }
156
188
  }
@@ -159,7 +191,7 @@
159
191
  "type": "array",
160
192
  "items": {
161
193
  "type": "object",
162
- "required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "trustScore", "riskFactors", "recommendation"],
194
+ "required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "trustScore", "riskFactors", "transitiveRiskScore", "riskyTransitiveDeps", "recommendation"],
163
195
  "additionalProperties": false,
164
196
  "properties": {
165
197
  "name": { "type": "string" },
@@ -170,6 +202,11 @@
170
202
  "explanation": { "type": "array", "items": { "type": "string" } },
171
203
  "trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
172
204
  "riskFactors": { "$ref": "#/properties/riskFactors" },
205
+ "transitiveRiskScore": { "type": "number" },
206
+ "riskyTransitiveDeps": {
207
+ "type": "array",
208
+ "items": { "$ref": "#/properties/riskTransitiveDependency" }
209
+ },
173
210
  "recommendation": { "$ref": "#/properties/recommendation" }
174
211
  }
175
212
  }
@@ -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
+ }