dep-brain 1.4.0 → 1.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/CHANGELOG.md +8 -0
- package/README.md +28 -10
- package/depbrain.output.schema.json +19 -1
- package/dist/checks/outdated.d.ts +4 -1
- package/dist/checks/outdated.js +180 -28
- package/dist/cli.js +40 -8
- package/dist/core/analyzer.d.ts +11 -1
- package/dist/core/analyzer.js +61 -10
- package/dist/reporters/console.js +11 -2
- package/dist/reporters/dashboard.js +30 -0
- package/dist/reporters/markdown.js +11 -2
- package/dist/utils/npm-api.d.ts +2 -0
- package/dist/utils/npm-api.js +3 -1
- package/package.json +1 -1
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
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
`dep-brain` is a CLI and library for explainable dependency intelligence in JavaScript and TypeScript projects.
|
|
8
8
|
|
|
9
|
+
Current release `1.5.1` adds upgrade-advice output, stepped major-version guidance, release-note links, and analysis output contract `1.6`.
|
|
10
|
+
|
|
9
11
|
## Vision
|
|
10
12
|
|
|
11
13
|
`dep-brain` aims to become a dependency decision engine:
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
- Score package trust using supply-chain metadata
|
|
26
28
|
- Generate a simple project health score
|
|
27
29
|
- Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
|
|
30
|
+
- Output upgrade-advice reports via `--advise`
|
|
28
31
|
- Gate CI with score and finding policies
|
|
29
32
|
- Compare new findings against a baseline report
|
|
30
33
|
|
|
@@ -34,7 +37,7 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
34
37
|
- Can I remove it safely?
|
|
35
38
|
- What should I fix first?
|
|
36
39
|
|
|
37
|
-
##
|
|
40
|
+
## 1.5 Highlights
|
|
38
41
|
|
|
39
42
|
- Duplicate dependency detection with lockfile instance tracking
|
|
40
43
|
- Unused dependency detection with runtime vs dev-tool heuristics
|
|
@@ -51,6 +54,7 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
51
54
|
- Markdown output via `--md`
|
|
52
55
|
- SARIF output via `--sarif`
|
|
53
56
|
- Static HTML dashboard via `--dashboard`
|
|
57
|
+
- Upgrade advisor output via `--advise`
|
|
54
58
|
- Ranked top issues via `--top`
|
|
55
59
|
- Baseline mode via `--baseline`
|
|
56
60
|
- Focused analysis via `--focus`
|
|
@@ -70,6 +74,7 @@ npx dep-brain analyze
|
|
|
70
74
|
npx dep-brain analyze --json
|
|
71
75
|
npx dep-brain analyze --md
|
|
72
76
|
npx dep-brain analyze --top
|
|
77
|
+
npx dep-brain analyze --advise
|
|
73
78
|
npx dep-brain analyze ./path-to-project
|
|
74
79
|
npx dep-brain analyze --config depbrain.config.json
|
|
75
80
|
npx dep-brain analyze --min-score 90 --fail-on-risks
|
|
@@ -164,7 +169,9 @@ Suggestions:
|
|
|
164
169
|
dep-brain analyze --json
|
|
165
170
|
```
|
|
166
171
|
|
|
167
|
-
Output includes `outputVersion` for schema stability
|
|
172
|
+
Output includes `outputVersion` for schema stability. `dep-brain@1.5.1` writes contract version `1.6`.
|
|
173
|
+
|
|
174
|
+
Validate against:
|
|
168
175
|
|
|
169
176
|
- `depbrain.output.schema.json`
|
|
170
177
|
|
|
@@ -182,6 +189,14 @@ dep-brain analyze --top
|
|
|
182
189
|
|
|
183
190
|
Shows the highest-priority actionable findings first, including confidence and next-step guidance.
|
|
184
191
|
|
|
192
|
+
## Upgrade Advice Output
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
dep-brain analyze --advise
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Shows recommended upgrade targets, stepped major-version paths, and release-note links when available.
|
|
199
|
+
|
|
185
200
|
## Dashboard Output
|
|
186
201
|
|
|
187
202
|
```bash
|
|
@@ -204,6 +219,8 @@ Writes a static HTML dashboard. Default path comes from `dashboard.outputPath`.
|
|
|
204
219
|
|
|
205
220
|
Built-in `license` plugin adds license counts under `extensions.license`. Failed plugin loads and hook errors are reported under `extensions.depBrain.plugins`.
|
|
206
221
|
|
|
222
|
+
Outdated results now include `advice` with `risk`, `recommendedTarget`, `intermediateSteps`, `releaseNotes`, and upgrade signals such as `semver_major`.
|
|
223
|
+
|
|
207
224
|
## Report From JSON
|
|
208
225
|
|
|
209
226
|
```bash
|
|
@@ -371,17 +388,18 @@ src/
|
|
|
371
388
|
|
|
372
389
|
## Product Direction
|
|
373
390
|
|
|
374
|
-
`dep-brain` is in
|
|
391
|
+
`dep-brain` is in `v1.5.1` production CLI stage, with current focus on actionable dependency decisions instead of raw issue lists.
|
|
392
|
+
|
|
393
|
+
Recent releases added:
|
|
375
394
|
|
|
376
|
-
-
|
|
377
|
-
-
|
|
378
|
-
-
|
|
379
|
-
-
|
|
380
|
-
- `v1.0`: stable CI, ecosystem exports, and production readiness
|
|
395
|
+
- transitive risk ownership and path tracing
|
|
396
|
+
- dashboard and plugin support
|
|
397
|
+
- baseline, focus, and CI workflows
|
|
398
|
+
- structured upgrade advice with release-note links
|
|
381
399
|
|
|
382
|
-
|
|
400
|
+
Project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
|
|
383
401
|
|
|
384
|
-
Risk findings
|
|
402
|
+
Risk findings include `trustScore`, structured `riskFactors`, `transitiveRiskScore`, and `riskyTransitiveDeps` path traces so teams can see which direct package introduces supply-chain risk.
|
|
385
403
|
|
|
386
404
|
## Repository Notes
|
|
387
405
|
|
|
@@ -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>;
|
package/dist/checks/outdated.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
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:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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("--
|
|
60
|
-
?
|
|
61
|
-
: flags.has("--
|
|
62
|
-
?
|
|
63
|
-
: flags.has("--
|
|
64
|
-
?
|
|
65
|
-
:
|
|
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
|
+
}
|
package/dist/core/analyzer.d.ts
CHANGED
|
@@ -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.
|
|
157
|
+
export declare const OUTPUT_VERSION = "1.6";
|
|
148
158
|
export interface ScoreBreakdown {
|
|
149
159
|
baseScore: number;
|
|
150
160
|
duplicates: number;
|
package/dist/core/analyzer.js
CHANGED
|
@@ -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.
|
|
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
|
|
447
|
-
const
|
|
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:
|
|
453
|
-
?
|
|
454
|
-
:
|
|
455
|
-
?
|
|
456
|
-
:
|
|
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, "&")
|
|
@@ -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
|
+
}
|
package/dist/utils/npm-api.d.ts
CHANGED
|
@@ -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>;
|
package/dist/utils/npm-api.js
CHANGED
|
@@ -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 {
|