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.
@@ -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 names = Object.keys({
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 results = await mapWithConcurrency(names, 8, async (name) => {
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, options.thresholds);
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
- reasons: assessment.reasons,
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
- return results
34
- .filter((item) => item !== null)
35
- .sort((left, right) => left.name.localeCompare(right.name));
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 assessRisk(metadata, dependencyType, thresholds) {
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 ? "high" : "medium",
301
+ priority: trustScore === "low" || confidence >= 0.8 || riskyTransitiveCount >= 2
302
+ ? "high"
303
+ : "medium",
167
304
  safety: "caution",
168
- summary: trustScore === "low"
169
- ? "Low trust package; review whether to replace, pin, or monitor it closely."
170
- : "Review package trust signals and decide whether to keep, replace, or monitor it.",
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("--json")
60
- ? JSON.stringify(reportData, null, 2)
61
- : flags.has("--sarif")
62
- ? renderSarifReport(reportData)
63
- : flags.has("--dashboard")
64
- ? renderDashboardReport(reportData)
65
- : renderMarkdownReport(reportData);
59
+ : flags.has("--advise")
60
+ ? renderUpgradeAdviceReport(reportData)
61
+ : flags.has("--json")
62
+ ? JSON.stringify(reportData, null, 2)
63
+ : flags.has("--sarif")
64
+ ? renderSarifReport(reportData)
65
+ : flags.has("--dashboard")
66
+ ? renderDashboardReport(reportData)
67
+ : renderMarkdownReport(reportData);
66
68
  await writeOutput(output, optionValues.get("--out"));
67
69
  return;
68
70
  }
@@ -141,6 +143,9 @@ async function main() {
141
143
  else if (flags.has("--top")) {
142
144
  output = renderTopIssuesReport(result);
143
145
  }
146
+ else if (flags.has("--advise")) {
147
+ output = renderUpgradeAdviceReport(result);
148
+ }
144
149
  else if (flags.has("--md")) {
145
150
  output = renderMarkdownReport(result);
146
151
  }
@@ -216,7 +221,7 @@ function printHelp() {
216
221
  console.log("");
217
222
  console.log("Usage:");
218
223
  console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
219
- console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--dashboard] [--out path]");
224
+ console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--advise] [--dashboard] [--out path]");
220
225
  console.log(" dep-brain config [path] [--config path]");
221
226
  console.log(" dep-brain init [--out depbrain.config.json]");
222
227
  console.log(" dep-brain help");
@@ -227,6 +232,7 @@ function printHelp() {
227
232
  console.log(" --md Output Markdown report");
228
233
  console.log(" --sarif Output SARIF format for Code Scanning");
229
234
  console.log(" --top Output the ranked top issues only");
235
+ console.log(" --advise Output upgrade advice for outdated dependencies");
230
236
  console.log(" --dashboard Write an HTML dashboard");
231
237
  console.log(" --dashboard-out <path> Write dashboard HTML to a custom path");
232
238
  console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
@@ -336,3 +342,29 @@ function renderTopIssuesReport(result) {
336
342
  }
337
343
  return lines.join("\n");
338
344
  }
345
+ function renderUpgradeAdviceReport(result) {
346
+ const lines = [];
347
+ lines.push("Upgrade Advice");
348
+ lines.push("");
349
+ if (!Array.isArray(result.outdated) || result.outdated.length === 0) {
350
+ lines.push("No outdated dependencies found.");
351
+ return lines.join("\n");
352
+ }
353
+ const sorted = [...result.outdated].sort((left, right) => compareAdviceRisk(right.advice.risk, left.advice.risk) ||
354
+ left.name.localeCompare(right.name));
355
+ for (const item of sorted) {
356
+ lines.push(`- ${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.advice.risk.toUpperCase()}]`);
357
+ lines.push(` Target: ${item.advice.recommendedTarget}`);
358
+ if (item.advice.intermediateSteps.length > 1) {
359
+ lines.push(` Steps: ${item.advice.intermediateSteps.join(" -> ")}`);
360
+ }
361
+ if (item.advice.releaseNotes[0]) {
362
+ lines.push(` Notes: ${item.advice.releaseNotes[0]}`);
363
+ }
364
+ }
365
+ return lines.join("\n");
366
+ }
367
+ function compareAdviceRisk(left, right) {
368
+ const rank = { high: 3, medium: 2, low: 1 };
369
+ return rank[left] - rank[right];
370
+ }
@@ -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.4";
157
+ export declare const OUTPUT_VERSION = "1.6";
137
158
  export interface ScoreBreakdown {
138
159
  baseScore: number;
139
160
  duplicates: number;
@@ -9,7 +9,7 @@ import { buildDependencyGraph } from "./graph-builder.js";
9
9
  import { PluginManager } from "./plugin-manager.js";
10
10
  import { calculateHealthScore, calculateScoreDeductions } from "./scorer.js";
11
11
  import { buildAnalysisContext } from "./context.js";
12
- export const OUTPUT_VERSION = "1.4";
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 priority = updateType === "major" ? "high" : updateType === "minor" ? "medium" : "low";
443
- const safety = updateType === "patch" ? "safe" : updateType === "minor" ? "caution" : "unknown";
447
+ const advice = normalizeOutdatedAdvice(issue.meta?.advice, issue.meta?.current, issue.meta?.latest);
448
+ const priority = advice.risk === "high"
449
+ ? "high"
450
+ : updateType === "major"
451
+ ? "high"
452
+ : updateType === "minor"
453
+ ? "medium"
454
+ : "low";
455
+ const safety = advice.risk === "high"
456
+ ? "unknown"
457
+ : updateType === "patch"
458
+ ? "safe"
459
+ : updateType === "minor"
460
+ ? "caution"
461
+ : "unknown";
444
462
  return {
445
463
  action: "upgrade",
446
464
  priority,
447
465
  safety,
448
- summary: updateType === "major"
449
- ? "New major version available; review breaking changes before upgrading."
450
- : updateType === "minor"
451
- ? "New minor version available; review release notes before upgrading."
452
- : updateType === "patch"
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 ? "high" : "medium",
481
+ priority: trustScore === "low" || confidence >= 0.79 || riskyTransitiveDeps.length >= 2
482
+ ? "high"
483
+ : "medium",
465
484
  safety: "caution",
466
- summary: trustScore === "low"
467
- ? "Low trust package; review whether to replace, pin, or monitor it closely."
468
- : "Review package trust signals and decide whether to keep, replace, or monitor it.",
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>;