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 +16 -0
- package/README.md +16 -1
- package/depbrain.output.schema.json +41 -4
- package/dist/checks/outdated.d.ts +4 -1
- package/dist/checks/outdated.js +180 -28
- package/dist/checks/risk.js +179 -25
- package/dist/cli.js +40 -8
- package/dist/core/analyzer.d.ts +22 -1
- package/dist/core/analyzer.js +107 -16
- package/dist/core/graph-builder.d.ts +1 -0
- package/dist/core/graph-builder.js +153 -58
- package/dist/index.d.ts +1 -1
- package/dist/reporters/console.js +20 -4
- package/dist/reporters/dashboard.js +74 -5
- package/dist/reporters/markdown.js +20 -4
- 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,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
|
|
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>;
|
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
|
+
}
|