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/dist/checks/risk.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { getPackageMetadata } from "../utils/npm-api.js";
|
|
2
2
|
export async function findRiskDependencies(graph, options = {}) {
|
|
3
3
|
const resolvePackageMetadata = options.resolvePackageMetadata ?? getPackageMetadata;
|
|
4
|
-
const
|
|
4
|
+
const thresholds = options.thresholds;
|
|
5
|
+
const allNames = Object.keys({
|
|
5
6
|
...graph.dependencies,
|
|
6
|
-
...graph.devDependencies
|
|
7
|
+
...graph.devDependencies,
|
|
8
|
+
...(graph.lockPackages ?? {})
|
|
7
9
|
});
|
|
8
|
-
const
|
|
10
|
+
const assessments = new Map();
|
|
11
|
+
const results = await mapWithConcurrency(allNames, 8, async (name) => {
|
|
9
12
|
const metadata = await resolvePackageMetadata(name);
|
|
10
13
|
if (!metadata) {
|
|
11
14
|
return null;
|
|
@@ -15,24 +18,25 @@ export async function findRiskDependencies(graph, options = {}) {
|
|
|
15
18
|
: graph.devDependencies[name]
|
|
16
19
|
? "devDependencies"
|
|
17
20
|
: "unknown";
|
|
18
|
-
const assessment = assessRisk(metadata, dependencyType,
|
|
19
|
-
if (!shouldReportRisk(assessment.trustScore, dependencyType)) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
21
|
+
const assessment = assessRisk(metadata, dependencyType, thresholds, 0);
|
|
22
22
|
return {
|
|
23
23
|
name,
|
|
24
|
-
|
|
25
|
-
confidence: assessment.confidence,
|
|
26
|
-
reasonCodes: assessment.reasonCodes,
|
|
27
|
-
explanation: assessment.reasons,
|
|
28
|
-
trustScore: assessment.trustScore,
|
|
29
|
-
riskFactors: assessment.riskFactors,
|
|
30
|
-
recommendation: buildRiskRecommendation(assessment.reasons, assessment.confidence, assessment.trustScore)
|
|
24
|
+
assessment
|
|
31
25
|
};
|
|
32
26
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
for (const result of results) {
|
|
28
|
+
if (result) {
|
|
29
|
+
assessments.set(result.name, result.assessment);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const directNames = Object.keys({
|
|
33
|
+
...graph.dependencies,
|
|
34
|
+
...graph.devDependencies
|
|
35
|
+
}).sort((left, right) => left.localeCompare(right));
|
|
36
|
+
const risks = directNames
|
|
37
|
+
.map((name) => buildDirectRiskEntry(name, graph, assessments, thresholds))
|
|
38
|
+
.filter((item) => item !== null);
|
|
39
|
+
return risks.sort((left, right) => left.name.localeCompare(right.name));
|
|
36
40
|
}
|
|
37
41
|
async function mapWithConcurrency(items, limit, mapper) {
|
|
38
42
|
const results = new Array(items.length);
|
|
@@ -68,12 +72,120 @@ export async function runRiskCheck(graph, options = {}) {
|
|
|
68
72
|
name: item.name,
|
|
69
73
|
reasons: item.reasons,
|
|
70
74
|
trustScore: item.trustScore,
|
|
71
|
-
riskFactors: item.riskFactors
|
|
75
|
+
riskFactors: item.riskFactors,
|
|
76
|
+
transitiveRiskScore: item.transitiveRiskScore,
|
|
77
|
+
riskyTransitiveDeps: item.riskyTransitiveDeps
|
|
72
78
|
}
|
|
73
79
|
}))
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
|
-
function
|
|
82
|
+
function buildDirectRiskEntry(name, graph, assessments, thresholds) {
|
|
83
|
+
const dependencyType = graph.dependencies[name]
|
|
84
|
+
? "dependencies"
|
|
85
|
+
: graph.devDependencies[name]
|
|
86
|
+
? "devDependencies"
|
|
87
|
+
: "unknown";
|
|
88
|
+
const selfAssessment = assessments.get(name) ?? buildUnknownAssessment(name, dependencyType);
|
|
89
|
+
const transitive = collectTransitiveRisks(name, graph, assessments);
|
|
90
|
+
const reasonCodes = [...selfAssessment.reasonCodes];
|
|
91
|
+
const reasons = [...selfAssessment.reasons];
|
|
92
|
+
const explanation = [...selfAssessment.reasons];
|
|
93
|
+
if (transitive.riskyTransitiveDeps.length > 0) {
|
|
94
|
+
reasons.push(`Introduces ${transitive.riskyTransitiveDeps.length} risky transitive dependenc${transitive.riskyTransitiveDeps.length === 1 ? "y" : "ies"}`);
|
|
95
|
+
reasonCodes.push("risky_transitive_dependencies");
|
|
96
|
+
explanation.push(`Transitive paths: ${transitive.riskyTransitiveDeps
|
|
97
|
+
.flatMap((item) => item.introducedByPaths)
|
|
98
|
+
.slice(0, 3)
|
|
99
|
+
.join("; ")}`);
|
|
100
|
+
}
|
|
101
|
+
if (transitive.transitiveDependencyCount > (thresholds?.transitiveBloatThreshold ?? 50)) {
|
|
102
|
+
reasons.push("Large transitive dependency tree");
|
|
103
|
+
reasonCodes.push("dependency_bloat");
|
|
104
|
+
explanation.push(`${name} introduces ${transitive.transitiveDependencyCount} transitive dependencies.`);
|
|
105
|
+
}
|
|
106
|
+
const transitiveRiskScore = transitive.riskyTransitiveDeps.reduce((total, item) => total + trustScoreWeight(item.trustScore), 0);
|
|
107
|
+
const combinedConfidence = Math.min(0.99, Math.max(selfAssessment.confidence, transitive.riskyTransitiveDeps.reduce((maxConfidence, item) => Math.max(maxConfidence, item.confidence), 0.5)));
|
|
108
|
+
const trustScore = combineTrustScores(selfAssessment.trustScore, transitive.highestTrustScore);
|
|
109
|
+
const shouldReport = shouldReportRisk(selfAssessment.trustScore, dependencyType) ||
|
|
110
|
+
transitive.riskyTransitiveDeps.length > 0 ||
|
|
111
|
+
transitive.transitiveDependencyCount > (thresholds?.transitiveBloatThreshold ?? 50);
|
|
112
|
+
if (!shouldReport || reasons.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
reasons,
|
|
118
|
+
confidence: combinedConfidence,
|
|
119
|
+
reasonCodes: dedupeStrings(reasonCodes),
|
|
120
|
+
explanation: dedupeStrings(explanation),
|
|
121
|
+
trustScore,
|
|
122
|
+
riskFactors: {
|
|
123
|
+
...selfAssessment.riskFactors,
|
|
124
|
+
dependencyType,
|
|
125
|
+
transitiveDependencyCount: transitive.transitiveDependencyCount,
|
|
126
|
+
riskyTransitiveCount: transitive.riskyTransitiveDeps.length
|
|
127
|
+
},
|
|
128
|
+
transitiveRiskScore,
|
|
129
|
+
riskyTransitiveDeps: transitive.riskyTransitiveDeps,
|
|
130
|
+
recommendation: buildRiskRecommendation(reasons, combinedConfidence, trustScore, transitive.riskyTransitiveDeps.length)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function collectTransitiveRisks(directName, graph, assessments) {
|
|
134
|
+
const visited = new Set();
|
|
135
|
+
const queue = (graph.lockDependencies?.[directName] ?? []).map((name) => ({
|
|
136
|
+
name,
|
|
137
|
+
path: [directName, name]
|
|
138
|
+
}));
|
|
139
|
+
const riskyByName = new Map();
|
|
140
|
+
let highestTrustScore = "high";
|
|
141
|
+
while (queue.length > 0) {
|
|
142
|
+
const current = queue.shift();
|
|
143
|
+
if (!current || visited.has(current.name)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
visited.add(current.name);
|
|
147
|
+
const assessment = assessments.get(current.name);
|
|
148
|
+
if (assessment && shouldReportRisk(assessment.trustScore, assessment.riskFactors.dependencyType)) {
|
|
149
|
+
highestTrustScore = combineTrustScores(highestTrustScore, assessment.trustScore);
|
|
150
|
+
const existing = riskyByName.get(current.name);
|
|
151
|
+
const pathTrace = current.path.join(" -> ");
|
|
152
|
+
if (existing) {
|
|
153
|
+
existing.introducedByPaths.push(pathTrace);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
riskyByName.set(current.name, {
|
|
157
|
+
name: current.name,
|
|
158
|
+
trustScore: assessment.trustScore,
|
|
159
|
+
confidence: assessment.confidence,
|
|
160
|
+
reasons: assessment.reasons,
|
|
161
|
+
introducedByPaths: [pathTrace]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const nextDependencies = graph.lockDependencies?.[current.name] ?? [];
|
|
166
|
+
for (const dependency of nextDependencies) {
|
|
167
|
+
if (!visited.has(dependency)) {
|
|
168
|
+
queue.push({
|
|
169
|
+
name: dependency,
|
|
170
|
+
path: [...current.path, dependency]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
transitiveDependencyCount: visited.size,
|
|
177
|
+
riskyTransitiveDeps: Array.from(riskyByName.values())
|
|
178
|
+
.map((item) => ({
|
|
179
|
+
...item,
|
|
180
|
+
introducedByPaths: dedupeStrings(item.introducedByPaths).slice(0, 3)
|
|
181
|
+
}))
|
|
182
|
+
.sort((left, right) => trustScoreWeight(right.trustScore) - trustScoreWeight(left.trustScore) ||
|
|
183
|
+
right.confidence - left.confidence ||
|
|
184
|
+
left.name.localeCompare(right.name)),
|
|
185
|
+
highestTrustScore
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function assessRisk(metadata, dependencyType, thresholds, transitiveDependencyCount) {
|
|
77
189
|
const reasons = [];
|
|
78
190
|
const reasonCodes = [];
|
|
79
191
|
let weight = 0;
|
|
@@ -127,6 +239,7 @@ function assessRisk(metadata, dependencyType, thresholds) {
|
|
|
127
239
|
? "medium"
|
|
128
240
|
: "high";
|
|
129
241
|
return {
|
|
242
|
+
name: "",
|
|
130
243
|
confidence,
|
|
131
244
|
trustScore,
|
|
132
245
|
reasons,
|
|
@@ -138,7 +251,29 @@ function assessRisk(metadata, dependencyType, thresholds) {
|
|
|
138
251
|
versionCount: metadata.versionCount,
|
|
139
252
|
recentReleaseCount: metadata.recentReleaseCount,
|
|
140
253
|
hasRepository: Boolean(metadata.repository),
|
|
141
|
-
dependencyType
|
|
254
|
+
dependencyType,
|
|
255
|
+
transitiveDependencyCount,
|
|
256
|
+
riskyTransitiveCount: 0
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function buildUnknownAssessment(name, dependencyType) {
|
|
261
|
+
return {
|
|
262
|
+
name,
|
|
263
|
+
confidence: 0.5,
|
|
264
|
+
trustScore: "high",
|
|
265
|
+
reasons: [],
|
|
266
|
+
reasonCodes: [],
|
|
267
|
+
riskFactors: {
|
|
268
|
+
daysSincePublish: null,
|
|
269
|
+
downloads: null,
|
|
270
|
+
maintainersCount: null,
|
|
271
|
+
versionCount: null,
|
|
272
|
+
recentReleaseCount: null,
|
|
273
|
+
hasRepository: false,
|
|
274
|
+
dependencyType,
|
|
275
|
+
transitiveDependencyCount: 0,
|
|
276
|
+
riskyTransitiveCount: 0
|
|
142
277
|
}
|
|
143
278
|
};
|
|
144
279
|
}
|
|
@@ -160,14 +295,33 @@ function shouldReportRisk(trustScore, dependencyType) {
|
|
|
160
295
|
}
|
|
161
296
|
return true;
|
|
162
297
|
}
|
|
163
|
-
function buildRiskRecommendation(reasons, confidence, trustScore) {
|
|
298
|
+
function buildRiskRecommendation(reasons, confidence, trustScore, riskyTransitiveCount) {
|
|
164
299
|
return {
|
|
165
300
|
action: "review",
|
|
166
|
-
priority: trustScore === "low" || confidence >= 0.8
|
|
301
|
+
priority: trustScore === "low" || confidence >= 0.8 || riskyTransitiveCount >= 2
|
|
302
|
+
? "high"
|
|
303
|
+
: "medium",
|
|
167
304
|
safety: "caution",
|
|
168
|
-
summary:
|
|
169
|
-
?
|
|
170
|
-
:
|
|
305
|
+
summary: riskyTransitiveCount > 0
|
|
306
|
+
? `Review this direct dependency and its transitive chain before upgrading or keeping it.`
|
|
307
|
+
: trustScore === "low"
|
|
308
|
+
? "Low trust package; review whether to replace, pin, or monitor it closely."
|
|
309
|
+
: "Review package trust signals and decide whether to keep, replace, or monitor it.",
|
|
171
310
|
reasons
|
|
172
311
|
};
|
|
173
312
|
}
|
|
313
|
+
function trustScoreWeight(value) {
|
|
314
|
+
if (value === "low") {
|
|
315
|
+
return 3;
|
|
316
|
+
}
|
|
317
|
+
if (value === "medium") {
|
|
318
|
+
return 2;
|
|
319
|
+
}
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
function combineTrustScores(left, right) {
|
|
323
|
+
return trustScoreWeight(left) >= trustScoreWeight(right) ? left : right;
|
|
324
|
+
}
|
|
325
|
+
function dedupeStrings(values) {
|
|
326
|
+
return Array.from(new Set(values));
|
|
327
|
+
}
|
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
|
@@ -43,8 +43,17 @@ export interface RiskFactors {
|
|
|
43
43
|
recentReleaseCount: number | null;
|
|
44
44
|
hasRepository: boolean;
|
|
45
45
|
dependencyType: "dependencies" | "devDependencies" | "unknown";
|
|
46
|
+
transitiveDependencyCount: number;
|
|
47
|
+
riskyTransitiveCount: number;
|
|
46
48
|
}
|
|
47
49
|
export type TrustScore = "high" | "medium" | "low";
|
|
50
|
+
export interface RiskTransitiveDependency {
|
|
51
|
+
name: string;
|
|
52
|
+
trustScore: TrustScore;
|
|
53
|
+
confidence: number;
|
|
54
|
+
reasons: string[];
|
|
55
|
+
introducedByPaths: string[];
|
|
56
|
+
}
|
|
48
57
|
export interface DuplicateDependency {
|
|
49
58
|
name: string;
|
|
50
59
|
versions: string[];
|
|
@@ -74,8 +83,18 @@ export interface OutdatedDependency {
|
|
|
74
83
|
confidence: number;
|
|
75
84
|
reasonCodes: string[];
|
|
76
85
|
explanation: string[];
|
|
86
|
+
advice: OutdatedDependencyAdvice;
|
|
77
87
|
recommendation: Recommendation;
|
|
78
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
|
+
}
|
|
79
98
|
export interface RiskDependency {
|
|
80
99
|
name: string;
|
|
81
100
|
reasons: string[];
|
|
@@ -85,6 +104,8 @@ export interface RiskDependency {
|
|
|
85
104
|
explanation: string[];
|
|
86
105
|
trustScore: TrustScore;
|
|
87
106
|
riskFactors: RiskFactors;
|
|
107
|
+
transitiveRiskScore: number;
|
|
108
|
+
riskyTransitiveDeps: RiskTransitiveDependency[];
|
|
88
109
|
recommendation: Recommendation;
|
|
89
110
|
}
|
|
90
111
|
export interface TopIssue {
|
|
@@ -133,7 +154,7 @@ export interface PackageAnalysisResult {
|
|
|
133
154
|
topIssues: TopIssue[];
|
|
134
155
|
extensions: Record<string, unknown>;
|
|
135
156
|
}
|
|
136
|
-
export declare const OUTPUT_VERSION = "1.
|
|
157
|
+
export declare const OUTPUT_VERSION = "1.6";
|
|
137
158
|
export interface ScoreBreakdown {
|
|
138
159
|
baseScore: number;
|
|
139
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
|
}
|
|
@@ -396,6 +397,10 @@ function mapRiskIssues(issues) {
|
|
|
396
397
|
explanation: normalizeStringArray(issue.explanation),
|
|
397
398
|
trustScore: normalizeTrustScore(issue.meta?.trustScore),
|
|
398
399
|
riskFactors: normalizeRiskFactors(issue.meta?.riskFactors),
|
|
400
|
+
transitiveRiskScore: typeof issue.meta?.transitiveRiskScore === "number"
|
|
401
|
+
? issue.meta.transitiveRiskScore
|
|
402
|
+
: 0,
|
|
403
|
+
riskyTransitiveDeps: normalizeRiskTransitiveDependencies(issue.meta?.riskyTransitiveDeps),
|
|
399
404
|
recommendation: buildRiskRecommendation(issue)
|
|
400
405
|
}));
|
|
401
406
|
}
|
|
@@ -439,19 +444,30 @@ function buildOutdatedRecommendation(issue) {
|
|
|
439
444
|
issue.meta?.updateType === "patch"
|
|
440
445
|
? issue.meta.updateType
|
|
441
446
|
: "unknown";
|
|
442
|
-
const
|
|
443
|
-
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";
|
|
444
462
|
return {
|
|
445
463
|
action: "upgrade",
|
|
446
464
|
priority,
|
|
447
465
|
safety,
|
|
448
|
-
summary:
|
|
449
|
-
?
|
|
450
|
-
:
|
|
451
|
-
?
|
|
452
|
-
:
|
|
453
|
-
? "Routine patch update available."
|
|
454
|
-
: "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}.`,
|
|
455
471
|
reasons: normalizeStringArray(issue.explanation)
|
|
456
472
|
};
|
|
457
473
|
}
|
|
@@ -459,13 +475,18 @@ function buildRiskRecommendation(issue) {
|
|
|
459
475
|
const reasons = normalizeStringArray(issue.explanation);
|
|
460
476
|
const confidence = normalizeConfidence(issue.confidence);
|
|
461
477
|
const trustScore = normalizeTrustScore(issue.meta?.trustScore);
|
|
478
|
+
const riskyTransitiveDeps = normalizeRiskTransitiveDependencies(issue.meta?.riskyTransitiveDeps);
|
|
462
479
|
return {
|
|
463
480
|
action: "review",
|
|
464
|
-
priority: trustScore === "low" || confidence >= 0.79
|
|
481
|
+
priority: trustScore === "low" || confidence >= 0.79 || riskyTransitiveDeps.length >= 2
|
|
482
|
+
? "high"
|
|
483
|
+
: "medium",
|
|
465
484
|
safety: "caution",
|
|
466
|
-
summary:
|
|
467
|
-
? "
|
|
468
|
-
:
|
|
485
|
+
summary: riskyTransitiveDeps.length > 0
|
|
486
|
+
? "Review this direct dependency and its transitive chain before upgrading or keeping it."
|
|
487
|
+
: trustScore === "low"
|
|
488
|
+
? "Low trust package; review whether to replace, pin, or monitor it closely."
|
|
489
|
+
: "Review package trust signals and decide whether to keep, replace, or monitor it.",
|
|
469
490
|
reasons
|
|
470
491
|
};
|
|
471
492
|
}
|
|
@@ -604,7 +625,9 @@ function normalizeRiskFactors(value) {
|
|
|
604
625
|
versionCount: null,
|
|
605
626
|
recentReleaseCount: null,
|
|
606
627
|
hasRepository: false,
|
|
607
|
-
dependencyType: "unknown"
|
|
628
|
+
dependencyType: "unknown",
|
|
629
|
+
transitiveDependencyCount: 0,
|
|
630
|
+
riskyTransitiveCount: 0
|
|
608
631
|
};
|
|
609
632
|
}
|
|
610
633
|
const factors = value;
|
|
@@ -617,7 +640,75 @@ function normalizeRiskFactors(value) {
|
|
|
617
640
|
hasRepository: factors.hasRepository === true,
|
|
618
641
|
dependencyType: factors.dependencyType === "dependencies" || factors.dependencyType === "devDependencies"
|
|
619
642
|
? factors.dependencyType
|
|
620
|
-
: "unknown"
|
|
643
|
+
: "unknown",
|
|
644
|
+
transitiveDependencyCount: typeof factors.transitiveDependencyCount === "number" ? factors.transitiveDependencyCount : 0,
|
|
645
|
+
riskyTransitiveCount: typeof factors.riskyTransitiveCount === "number" ? factors.riskyTransitiveCount : 0
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function normalizeRiskTransitiveDependencies(value) {
|
|
649
|
+
if (!Array.isArray(value)) {
|
|
650
|
+
return [];
|
|
651
|
+
}
|
|
652
|
+
return value
|
|
653
|
+
.map((entry) => {
|
|
654
|
+
if (!entry || typeof entry !== "object") {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
const item = entry;
|
|
658
|
+
if (typeof item.name !== "string" ||
|
|
659
|
+
(item.trustScore !== "high" && item.trustScore !== "medium" && item.trustScore !== "low") ||
|
|
660
|
+
typeof item.confidence !== "number" ||
|
|
661
|
+
!Array.isArray(item.reasons) ||
|
|
662
|
+
!Array.isArray(item.introducedByPaths)) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
name: item.name,
|
|
667
|
+
trustScore: item.trustScore,
|
|
668
|
+
confidence: normalizeConfidence(item.confidence),
|
|
669
|
+
reasons: item.reasons.filter((reason) => typeof reason === "string"),
|
|
670
|
+
introducedByPaths: item.introducedByPaths.filter((trace) => typeof trace === "string")
|
|
671
|
+
};
|
|
672
|
+
})
|
|
673
|
+
.filter((entry) => entry !== null);
|
|
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
|
|
621
712
|
};
|
|
622
713
|
}
|
|
623
714
|
function normalizeWorkspaceUsage(value) {
|
|
@@ -11,5 +11,6 @@ export interface DependencyGraph {
|
|
|
11
11
|
overrides: Record<string, unknown>;
|
|
12
12
|
scripts: Record<string, string>;
|
|
13
13
|
lockPackages: Record<string, LockPackageInstance[]>;
|
|
14
|
+
lockDependencies: Record<string, string[]>;
|
|
14
15
|
}
|
|
15
16
|
export declare function buildDependencyGraph(rootDir: string): Promise<DependencyGraph>;
|