dep-brain 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,3 +7,5 @@ All notable changes to this project will be documented in this file.
7
7
  - Workspace-aware analysis for npm workspaces.
8
8
  - Config loading and CI policy controls.
9
9
  - Improved duplicate detection and unused dependency heuristics.
10
+ - Actionable recommendations for unused, duplicate, outdated, and risk findings.
11
+ - Ranked top-issues summary output with `--top`.
package/README.md CHANGED
@@ -53,6 +53,7 @@ dep-brain analyze
53
53
  npx dep-brain analyze
54
54
  npx dep-brain analyze --json
55
55
  npx dep-brain analyze --md
56
+ npx dep-brain analyze --top
56
57
  npx dep-brain analyze ./path-to-project
57
58
  npx dep-brain analyze --config depbrain.config.json
58
59
  npx dep-brain analyze --min-score 90 --fail-on-risks
@@ -115,6 +116,14 @@ Output includes `outputVersion` for schema stability and can be validated with:
115
116
  dep-brain analyze --md
116
117
  ```
117
118
 
119
+ ## Top Issues Output
120
+
121
+ ```bash
122
+ dep-brain analyze --top
123
+ ```
124
+
125
+ Shows the highest-priority actionable findings first, including confidence and next-step guidance.
126
+
118
127
  ## Report From JSON
119
128
 
120
129
  ```bash
@@ -2,9 +2,21 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "title": "Dependency Brain Analysis Output",
4
4
  "type": "object",
5
- "required": ["outputVersion", "rootDir", "score", "scoreBreakdown", "policy", "duplicates", "unused", "outdated", "risks", "suggestions", "config"],
5
+ "required": ["outputVersion", "rootDir", "score", "scoreBreakdown", "policy", "duplicates", "unused", "outdated", "risks", "suggestions", "topIssues", "config"],
6
6
  "additionalProperties": false,
7
7
  "properties": {
8
+ "recommendation": {
9
+ "type": "object",
10
+ "required": ["action", "priority", "safety", "summary", "reasons"],
11
+ "additionalProperties": false,
12
+ "properties": {
13
+ "action": { "type": "string", "enum": ["remove", "consolidate", "upgrade", "review"] },
14
+ "priority": { "type": "string", "enum": ["high", "medium", "low"] },
15
+ "safety": { "type": "string", "enum": ["safe", "caution", "unknown"] },
16
+ "summary": { "type": "string" },
17
+ "reasons": { "type": "array", "items": { "type": "string" } }
18
+ }
19
+ },
8
20
  "outputVersion": { "type": "string" },
9
21
  "rootDir": { "type": "string" },
10
22
  "score": { "type": "number" },
@@ -44,7 +56,7 @@
44
56
  "type": "array",
45
57
  "items": {
46
58
  "type": "object",
47
- "required": ["name", "versions", "instances", "confidence", "reasonCodes", "explanation"],
59
+ "required": ["name", "versions", "instances", "confidence", "reasonCodes", "explanation", "recommendation"],
48
60
  "additionalProperties": false,
49
61
  "properties": {
50
62
  "name": { "type": "string" },
@@ -52,6 +64,7 @@
52
64
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
53
65
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
54
66
  "explanation": { "type": "array", "items": { "type": "string" } },
67
+ "recommendation": { "$ref": "#/properties/recommendation" },
55
68
  "instances": {
56
69
  "type": "array",
57
70
  "items": {
@@ -71,7 +84,7 @@
71
84
  "type": "array",
72
85
  "items": {
73
86
  "type": "object",
74
- "required": ["name", "section", "confidence", "reasonCodes", "explanation"],
87
+ "required": ["name", "section", "confidence", "reasonCodes", "explanation", "recommendation"],
75
88
  "additionalProperties": false,
76
89
  "properties": {
77
90
  "name": { "type": "string" },
@@ -79,7 +92,8 @@
79
92
  "package": { "type": "string" },
80
93
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
81
94
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
82
- "explanation": { "type": "array", "items": { "type": "string" } }
95
+ "explanation": { "type": "array", "items": { "type": "string" } },
96
+ "recommendation": { "$ref": "#/properties/recommendation" }
83
97
  }
84
98
  }
85
99
  },
@@ -87,7 +101,7 @@
87
101
  "type": "array",
88
102
  "items": {
89
103
  "type": "object",
90
- "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation"],
104
+ "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation", "recommendation"],
91
105
  "additionalProperties": false,
92
106
  "properties": {
93
107
  "name": { "type": "string" },
@@ -97,7 +111,8 @@
97
111
  "package": { "type": "string" },
98
112
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
99
113
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
100
- "explanation": { "type": "array", "items": { "type": "string" } }
114
+ "explanation": { "type": "array", "items": { "type": "string" } },
115
+ "recommendation": { "$ref": "#/properties/recommendation" }
101
116
  }
102
117
  }
103
118
  },
@@ -105,7 +120,7 @@
105
120
  "type": "array",
106
121
  "items": {
107
122
  "type": "object",
108
- "required": ["name", "reasons", "confidence", "reasonCodes", "explanation"],
123
+ "required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "recommendation"],
109
124
  "additionalProperties": false,
110
125
  "properties": {
111
126
  "name": { "type": "string" },
@@ -113,11 +128,29 @@
113
128
  "package": { "type": "string" },
114
129
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
115
130
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
116
- "explanation": { "type": "array", "items": { "type": "string" } }
131
+ "explanation": { "type": "array", "items": { "type": "string" } },
132
+ "recommendation": { "$ref": "#/properties/recommendation" }
117
133
  }
118
134
  }
119
135
  },
120
136
  "suggestions": { "type": "array", "items": { "type": "string" } },
137
+ "topIssues": {
138
+ "type": "array",
139
+ "items": {
140
+ "type": "object",
141
+ "required": ["kind", "name", "priority", "confidence", "summary", "recommendation"],
142
+ "additionalProperties": false,
143
+ "properties": {
144
+ "kind": { "type": "string", "enum": ["unused", "duplicate", "outdated", "risk"] },
145
+ "name": { "type": "string" },
146
+ "package": { "type": "string" },
147
+ "priority": { "type": "string", "enum": ["high", "medium", "low"] },
148
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
149
+ "summary": { "type": "string" },
150
+ "recommendation": { "$ref": "#/properties/recommendation" }
151
+ }
152
+ }
153
+ },
121
154
  "config": { "type": "object" },
122
155
  "packages": { "type": "array" }
123
156
  }
@@ -12,11 +12,27 @@ export async function findDuplicateDependencies(graph) {
12
12
  explanation: [
13
13
  `Multiple versions of ${name} were found in the lockfile.`,
14
14
  "The package is installed from more than one dependency path."
15
- ]
15
+ ],
16
+ recommendation: buildDuplicateRecommendation(Array.from(new Set(instances.map((instance) => instance.version))).sort(), instances.length)
16
17
  }))
17
18
  .filter((dependency) => dependency.versions.length > 1)
18
19
  .sort((left, right) => left.name.localeCompare(right.name));
19
20
  }
21
+ function buildDuplicateRecommendation(versions, instanceCount) {
22
+ const targetVersion = versions[versions.length - 1];
23
+ return {
24
+ action: "consolidate",
25
+ priority: versions.length >= 3 ? "high" : "medium",
26
+ safety: "caution",
27
+ summary: targetVersion
28
+ ? `Consolidate toward ${targetVersion}; ${instanceCount} installation paths are affected.`
29
+ : "Consolidate duplicate versions to a single target version.",
30
+ reasons: [
31
+ "Multiple versions of the same package were detected in the lockfile.",
32
+ "Consolidating versions can reduce drift and simplify upgrades."
33
+ ]
34
+ };
35
+ }
20
36
  export async function runDuplicateCheck(graph) {
21
37
  const duplicates = await findDuplicateDependencies(graph);
22
38
  return {
@@ -11,26 +11,45 @@ export async function findOutdatedDependencies(graph, options = {}) {
11
11
  if (!latest || latest === normalized) {
12
12
  return null;
13
13
  }
14
+ const updateType = classifyUpdateType(normalized, latest);
14
15
  return {
15
16
  name,
16
17
  current,
17
18
  latest,
18
- updateType: classifyUpdateType(normalized, latest),
19
+ updateType,
19
20
  confidence: 0.97,
20
21
  reasonCodes: [
21
22
  "latest_registry_version_newer",
22
- `update_type_${classifyUpdateType(normalized, latest)}`
23
+ `update_type_${updateType}`
23
24
  ],
24
25
  explanation: [
25
26
  "The npm registry reports a newer published version than the one declared in this project.",
26
- `The change is classified as a ${classifyUpdateType(normalized, latest)} update.`
27
- ]
27
+ `The change is classified as a ${updateType} update.`
28
+ ],
29
+ recommendation: buildOutdatedRecommendation(updateType)
28
30
  };
29
31
  }));
30
32
  return results
31
33
  .filter((item) => item !== null)
32
34
  .sort((left, right) => left.name.localeCompare(right.name));
33
35
  }
36
+ function buildOutdatedRecommendation(updateType) {
37
+ return {
38
+ action: "upgrade",
39
+ priority: updateType === "major" ? "high" : updateType === "minor" ? "medium" : "low",
40
+ safety: updateType === "patch" ? "safe" : updateType === "minor" ? "caution" : "unknown",
41
+ summary: updateType === "major"
42
+ ? "New major version available; review breaking changes before upgrading."
43
+ : updateType === "minor"
44
+ ? "New minor version available; review release notes before upgrading."
45
+ : updateType === "patch"
46
+ ? "Routine patch update available."
47
+ : "Newer version available; review upgrade impact.",
48
+ reasons: [
49
+ "A newer published version is available in the registry."
50
+ ]
51
+ };
52
+ }
34
53
  export async function runOutdatedCheck(graph) {
35
54
  const outdated = await findOutdatedDependencies(graph);
36
55
  return {
@@ -28,13 +28,23 @@ export async function findRiskDependencies(graph) {
28
28
  reasons,
29
29
  confidence: calculateRiskConfidence(reasons),
30
30
  reasonCodes: reasons.map(toRiskReasonCode),
31
- explanation: reasons
31
+ explanation: reasons,
32
+ recommendation: buildRiskRecommendation(reasons, calculateRiskConfidence(reasons))
32
33
  };
33
34
  }));
34
35
  return results
35
36
  .filter((item) => item !== null)
36
37
  .sort((left, right) => left.name.localeCompare(right.name));
37
38
  }
39
+ function buildRiskRecommendation(reasons, confidence) {
40
+ return {
41
+ action: "review",
42
+ priority: confidence >= 0.79 ? "high" : "medium",
43
+ safety: "caution",
44
+ summary: "Review package trust signals and decide whether to keep, replace, or monitor it.",
45
+ reasons
46
+ };
47
+ }
38
48
  export async function runRiskCheck(graph) {
39
49
  const risks = await findRiskDependencies(graph);
40
50
  return {
@@ -144,6 +144,22 @@ function buildUnusedDependency(name, section) {
144
144
  "No import or require usage was found in scanned source files.",
145
145
  "No matching reference was found in recognized config files.",
146
146
  "No matching binary or package reference was found in package scripts."
147
+ ],
148
+ recommendation: buildUnusedRecommendation(section)
149
+ };
150
+ }
151
+ function buildUnusedRecommendation(section) {
152
+ const safety = section === "devDependencies" ? "safe" : "caution";
153
+ return {
154
+ action: "remove",
155
+ priority: section === "dependencies" ? "high" : "medium",
156
+ safety,
157
+ summary: safety === "safe"
158
+ ? `Safe to remove from ${section}.`
159
+ : `Likely removable from ${section}, but review before deleting.`,
160
+ reasons: [
161
+ "No code usage was found in scanned files.",
162
+ "No config or script reference was detected."
147
163
  ]
148
164
  };
149
165
  }
package/dist/cli.js CHANGED
@@ -51,9 +51,11 @@ async function main() {
51
51
  const resolvedFrom = resolveUserPath(fromPath);
52
52
  const raw = await fs.readFile(resolvedFrom, "utf8");
53
53
  const reportData = JSON.parse(raw);
54
- const output = flags.has("--json")
55
- ? JSON.stringify(reportData, null, 2)
56
- : renderMarkdownReport(reportData);
54
+ const output = flags.has("--top")
55
+ ? renderTopIssuesReport(reportData)
56
+ : flags.has("--json")
57
+ ? JSON.stringify(reportData, null, 2)
58
+ : renderMarkdownReport(reportData);
57
59
  await writeOutput(output, optionValues.get("--out"));
58
60
  return;
59
61
  }
@@ -107,6 +109,9 @@ async function main() {
107
109
  if (flags.has("--json")) {
108
110
  output = renderJsonReport(result);
109
111
  }
112
+ else if (flags.has("--top")) {
113
+ output = renderTopIssuesReport(result);
114
+ }
110
115
  else if (flags.has("--md")) {
111
116
  output = renderMarkdownReport(result);
112
117
  }
@@ -173,8 +178,8 @@ function printHelp() {
173
178
  console.log("Dependency Brain");
174
179
  console.log("");
175
180
  console.log("Usage:");
176
- console.log(" dep-brain analyze [path] [--json] [--md] [--out path] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
177
- console.log(" dep-brain report --from <file> [--md] [--json] [--out path]");
181
+ console.log(" dep-brain analyze [path] [--json] [--md] [--top] [--out path] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
182
+ console.log(" dep-brain report --from <file> [--md] [--json] [--top] [--out path]");
178
183
  console.log(" dep-brain config [path] [--config path]");
179
184
  console.log(" dep-brain help");
180
185
  console.log(" dep-brain --version");
@@ -182,6 +187,7 @@ function printHelp() {
182
187
  console.log("Options:");
183
188
  console.log(" --json Output JSON for analysis");
184
189
  console.log(" --md Output Markdown report");
190
+ console.log(" --top Output the ranked top issues only");
185
191
  console.log(" --config <path> Path to depbrain.config.json");
186
192
  console.log(" --from <file> Read analysis JSON from file");
187
193
  console.log(" --out <path> Write output to a file");
@@ -218,3 +224,18 @@ function sanitizeForLog(value) {
218
224
  function resolveUserPath(value) {
219
225
  return path.resolve(process.cwd(), value);
220
226
  }
227
+ function renderTopIssuesReport(result) {
228
+ const lines = [];
229
+ lines.push("Top Issues");
230
+ lines.push("");
231
+ if (!Array.isArray(result.topIssues) || result.topIssues.length === 0) {
232
+ lines.push("No actionable issues found.");
233
+ return lines.join("\n");
234
+ }
235
+ for (const [index, item] of result.topIssues.entries()) {
236
+ lines.push(`${index + 1}. [${item.priority.toUpperCase()}] ${item.kind} ${item.name}${item.package ? ` [${item.package}]` : ""}`);
237
+ lines.push(` Confidence: ${Math.round(item.confidence * 100)}%`);
238
+ lines.push(` Next: ${item.recommendation.summary}`);
239
+ }
240
+ return lines.join("\n");
241
+ }
@@ -8,6 +8,13 @@ export interface DuplicateInstance {
8
8
  path: string;
9
9
  version: string;
10
10
  }
11
+ export interface Recommendation {
12
+ action: "remove" | "consolidate" | "upgrade" | "review";
13
+ priority: "high" | "medium" | "low";
14
+ safety: "safe" | "caution" | "unknown";
15
+ summary: string;
16
+ reasons: string[];
17
+ }
11
18
  export interface DuplicateDependency {
12
19
  name: string;
13
20
  versions: string[];
@@ -15,6 +22,7 @@ export interface DuplicateDependency {
15
22
  confidence: number;
16
23
  reasonCodes: string[];
17
24
  explanation: string[];
25
+ recommendation: Recommendation;
18
26
  }
19
27
  export interface UnusedDependency {
20
28
  name: string;
@@ -23,6 +31,7 @@ export interface UnusedDependency {
23
31
  confidence: number;
24
32
  reasonCodes: string[];
25
33
  explanation: string[];
34
+ recommendation: Recommendation;
26
35
  }
27
36
  export interface OutdatedDependency {
28
37
  name: string;
@@ -33,6 +42,7 @@ export interface OutdatedDependency {
33
42
  confidence: number;
34
43
  reasonCodes: string[];
35
44
  explanation: string[];
45
+ recommendation: Recommendation;
36
46
  }
37
47
  export interface RiskDependency {
38
48
  name: string;
@@ -41,6 +51,16 @@ export interface RiskDependency {
41
51
  confidence: number;
42
52
  reasonCodes: string[];
43
53
  explanation: string[];
54
+ recommendation: Recommendation;
55
+ }
56
+ export interface TopIssue {
57
+ kind: "unused" | "duplicate" | "outdated" | "risk";
58
+ name: string;
59
+ package?: string;
60
+ priority: "high" | "medium" | "low";
61
+ confidence: number;
62
+ summary: string;
63
+ recommendation: Recommendation;
44
64
  }
45
65
  export interface AnalysisResult {
46
66
  outputVersion: string;
@@ -53,6 +73,7 @@ export interface AnalysisResult {
53
73
  outdated: OutdatedDependency[];
54
74
  risks: RiskDependency[];
55
75
  suggestions: string[];
76
+ topIssues: TopIssue[];
56
77
  config: DepBrainConfig;
57
78
  packages?: PackageAnalysisResult[];
58
79
  }
@@ -71,8 +92,9 @@ export interface PackageAnalysisResult {
71
92
  outdated: OutdatedDependency[];
72
93
  risks: RiskDependency[];
73
94
  suggestions: string[];
95
+ topIssues: TopIssue[];
74
96
  }
75
- export declare const OUTPUT_VERSION = "1.1";
97
+ export declare const OUTPUT_VERSION = "1.2";
76
98
  export interface ScoreBreakdown {
77
99
  baseScore: number;
78
100
  duplicates: number;
@@ -8,7 +8,7 @@ import { findWorkspacePackages } from "../utils/workspaces.js";
8
8
  import { buildDependencyGraph } from "./graph-builder.js";
9
9
  import { calculateHealthScore } from "./scorer.js";
10
10
  import { buildAnalysisContext } from "./context.js";
11
- export const OUTPUT_VERSION = "1.1";
11
+ export const OUTPUT_VERSION = "1.2";
12
12
  export async function analyzeProject(options = {}) {
13
13
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
14
14
  const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
@@ -68,6 +68,7 @@ export async function analyzeProject(options = {}) {
68
68
  outdated,
69
69
  risks,
70
70
  suggestions,
71
+ topIssues: buildTopIssues({ duplicates, unused, outdated, risks }),
71
72
  config,
72
73
  packages
73
74
  };
@@ -186,6 +187,12 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
186
187
  outdated: scopedOutdated,
187
188
  risks: scopedRisks,
188
189
  suggestions,
190
+ topIssues: buildTopIssues({
191
+ duplicates,
192
+ unused: scopedUnused,
193
+ outdated: scopedOutdated,
194
+ risks: scopedRisks
195
+ }),
189
196
  config
190
197
  };
191
198
  }
@@ -267,7 +274,8 @@ function mapDuplicateIssues(issues) {
267
274
  : [],
268
275
  confidence: normalizeConfidence(issue.confidence),
269
276
  reasonCodes: normalizeStringArray(issue.reasonCodes),
270
- explanation: normalizeStringArray(issue.explanation)
277
+ explanation: normalizeStringArray(issue.explanation),
278
+ recommendation: buildDuplicateRecommendation(issue)
271
279
  }));
272
280
  }
273
281
  function mapUnusedIssues(issues) {
@@ -276,7 +284,8 @@ function mapUnusedIssues(issues) {
276
284
  section: issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies",
277
285
  confidence: normalizeConfidence(issue.confidence),
278
286
  reasonCodes: normalizeStringArray(issue.reasonCodes),
279
- explanation: normalizeStringArray(issue.explanation)
287
+ explanation: normalizeStringArray(issue.explanation),
288
+ recommendation: buildUnusedRecommendation(issue)
280
289
  }));
281
290
  }
282
291
  function mapOutdatedIssues(issues) {
@@ -289,7 +298,8 @@ function mapOutdatedIssues(issues) {
289
298
  : "unknown",
290
299
  confidence: normalizeConfidence(issue.confidence),
291
300
  reasonCodes: normalizeStringArray(issue.reasonCodes),
292
- explanation: normalizeStringArray(issue.explanation)
301
+ explanation: normalizeStringArray(issue.explanation),
302
+ recommendation: buildOutdatedRecommendation(issue)
293
303
  }));
294
304
  }
295
305
  function mapRiskIssues(issues) {
@@ -298,9 +308,123 @@ function mapRiskIssues(issues) {
298
308
  reasons: Array.isArray(issue.meta?.reasons) ? issue.meta?.reasons : [],
299
309
  confidence: normalizeConfidence(issue.confidence),
300
310
  reasonCodes: normalizeStringArray(issue.reasonCodes),
301
- explanation: normalizeStringArray(issue.explanation)
311
+ explanation: normalizeStringArray(issue.explanation),
312
+ recommendation: buildRiskRecommendation(issue)
302
313
  }));
303
314
  }
315
+ function buildUnusedRecommendation(issue) {
316
+ const confidence = normalizeConfidence(issue.confidence);
317
+ const section = issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies";
318
+ const safety = section === "devDependencies" || confidence >= 0.88
319
+ ? "safe"
320
+ : confidence >= 0.7
321
+ ? "caution"
322
+ : "unknown";
323
+ return {
324
+ action: "remove",
325
+ priority: confidence >= 0.88 ? "high" : "medium",
326
+ safety,
327
+ summary: safety === "safe"
328
+ ? `Safe to remove from ${section}.`
329
+ : safety === "caution"
330
+ ? `Likely removable from ${section}, but review before deleting.`
331
+ : `Potentially removable from ${section}, but evidence is limited.`,
332
+ reasons: normalizeStringArray(issue.explanation)
333
+ };
334
+ }
335
+ function buildDuplicateRecommendation(issue) {
336
+ const versions = Array.isArray(issue.meta?.versions) ? issue.meta.versions : [];
337
+ const targetVersion = versions[versions.length - 1];
338
+ const instances = Array.isArray(issue.meta?.instances) ? issue.meta.instances.length : 0;
339
+ return {
340
+ action: "consolidate",
341
+ priority: versions.length >= 3 ? "high" : "medium",
342
+ safety: "caution",
343
+ summary: targetVersion
344
+ ? `Consolidate toward ${targetVersion}; ${instances} installation paths are affected.`
345
+ : "Consolidate duplicate versions to a single target version.",
346
+ reasons: normalizeStringArray(issue.explanation)
347
+ };
348
+ }
349
+ function buildOutdatedRecommendation(issue) {
350
+ const updateType = issue.meta?.updateType === "major" ||
351
+ issue.meta?.updateType === "minor" ||
352
+ issue.meta?.updateType === "patch"
353
+ ? issue.meta.updateType
354
+ : "unknown";
355
+ const priority = updateType === "major" ? "high" : updateType === "minor" ? "medium" : "low";
356
+ const safety = updateType === "patch" ? "safe" : updateType === "minor" ? "caution" : "unknown";
357
+ return {
358
+ action: "upgrade",
359
+ priority,
360
+ safety,
361
+ summary: updateType === "major"
362
+ ? "New major version available; review breaking changes before upgrading."
363
+ : updateType === "minor"
364
+ ? "New minor version available; review release notes before upgrading."
365
+ : updateType === "patch"
366
+ ? "Routine patch update available."
367
+ : "Newer version available; review upgrade impact.",
368
+ reasons: normalizeStringArray(issue.explanation)
369
+ };
370
+ }
371
+ function buildRiskRecommendation(issue) {
372
+ const reasons = normalizeStringArray(issue.explanation);
373
+ const confidence = normalizeConfidence(issue.confidence);
374
+ return {
375
+ action: "review",
376
+ priority: confidence >= 0.79 ? "high" : "medium",
377
+ safety: "caution",
378
+ summary: "Review package trust signals and decide whether to keep, replace, or monitor it.",
379
+ reasons
380
+ };
381
+ }
382
+ function buildTopIssues(input) {
383
+ const issues = [
384
+ ...input.unused.map((item) => ({
385
+ kind: "unused",
386
+ name: item.name,
387
+ package: item.package,
388
+ priority: item.recommendation.priority,
389
+ confidence: item.confidence,
390
+ summary: item.recommendation.summary,
391
+ recommendation: item.recommendation
392
+ })),
393
+ ...input.duplicates.map((item) => ({
394
+ kind: "duplicate",
395
+ name: item.name,
396
+ priority: item.recommendation.priority,
397
+ confidence: item.confidence,
398
+ summary: item.recommendation.summary,
399
+ recommendation: item.recommendation
400
+ })),
401
+ ...input.outdated.map((item) => ({
402
+ kind: "outdated",
403
+ name: item.name,
404
+ package: item.package,
405
+ priority: item.recommendation.priority,
406
+ confidence: item.confidence,
407
+ summary: item.recommendation.summary,
408
+ recommendation: item.recommendation
409
+ })),
410
+ ...input.risks.map((item) => ({
411
+ kind: "risk",
412
+ name: item.name,
413
+ package: item.package,
414
+ priority: item.recommendation.priority,
415
+ confidence: item.confidence,
416
+ summary: item.recommendation.summary,
417
+ recommendation: item.recommendation
418
+ }))
419
+ ];
420
+ return issues
421
+ .sort((left, right) => comparePriority(right.priority, left.priority) || right.confidence - left.confidence)
422
+ .slice(0, 5);
423
+ }
424
+ function comparePriority(left, right) {
425
+ const rank = { high: 3, medium: 2, low: 1 };
426
+ return rank[left] - rank[right];
427
+ }
304
428
  function normalizeConfidence(value) {
305
429
  if (typeof value !== "number" || Number.isNaN(value)) {
306
430
  return 0.5;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { analyzeProject } from "./core/analyzer.js";
2
- export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, ScoreBreakdown, RiskDependency, UnusedDependency } from "./core/analyzer.js";
2
+ export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, ScoreBreakdown, RiskDependency, TopIssue, UnusedDependency } from "./core/analyzer.js";
3
3
  export { OUTPUT_VERSION } from "./core/analyzer.js";
4
4
  export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
5
5
  export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
@@ -1,5 +1,12 @@
1
1
  export function renderConsoleReport(result) {
2
2
  const lines = [];
3
+ if (result.topIssues.length > 0) {
4
+ lines.push("Top Issues:");
5
+ for (const item of result.topIssues) {
6
+ lines.push(`- [${item.priority.toUpperCase()}] ${item.kind} ${item.name}${item.package ? ` [${item.package}]` : ""} | confidence ${Math.round(item.confidence * 100)}% | ${item.summary}`);
7
+ }
8
+ lines.push("");
9
+ }
3
10
  lines.push(`Project Health: ${result.score}/100`);
4
11
  lines.push(`Path: ${result.rootDir}`);
5
12
  lines.push(`Policy: ${result.policy.passed ? "PASS" : "FAIL"}`);
@@ -16,16 +23,16 @@ export function renderConsoleReport(result) {
16
23
  lines.push(`- ${pkg.name}: ${pkg.score}/100, D:${pkg.duplicates.length} U:${pkg.unused.length} O:${pkg.outdated.length} R:${pkg.risks.length}`);
17
24
  }
18
25
  }
19
- appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation)));
26
+ appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation, item.recommendation)));
20
27
  appendSection(lines, "Unused dependencies", result.unused.map((item) => formatEntry(item.package
21
28
  ? `${item.name} (${item.section}) [${item.package}]`
22
- : `${item.name} (${item.section})`, item.confidence, item.explanation)));
29
+ : `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
23
30
  appendSection(lines, "Outdated dependencies", result.outdated.map((item) => formatEntry(item.package
24
31
  ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
25
- : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation)));
32
+ : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
26
33
  appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
27
34
  ? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
28
- : `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation)));
35
+ : `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation, item.recommendation)));
29
36
  appendSection(lines, "Policy reasons", result.policy.reasons);
30
37
  if (result.suggestions.length > 0) {
31
38
  lines.push("");
@@ -50,7 +57,10 @@ function appendSection(lines, title, entries) {
50
57
  lines.push(`- ${entry}`);
51
58
  }
52
59
  }
53
- function formatEntry(label, confidence, explanation) {
60
+ function formatEntry(label, confidence, explanation, recommendation) {
54
61
  const reasonSummary = explanation.length > 0 ? ` | why: ${explanation.join("; ")}` : "";
55
- return `${label} | confidence ${Math.round(confidence * 100)}%${reasonSummary}`;
62
+ const recommendationSummary = recommendation
63
+ ? ` | next: ${recommendation.summary}`
64
+ : "";
65
+ return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
56
66
  }
@@ -2,6 +2,13 @@ export function renderMarkdownReport(result) {
2
2
  const lines = [];
3
3
  lines.push(`# Dependency Brain Report`);
4
4
  lines.push("");
5
+ if (result.topIssues.length > 0) {
6
+ lines.push("## Top Issues");
7
+ for (const item of result.topIssues) {
8
+ lines.push(`- **${item.priority.toUpperCase()}** ${item.kind} \`${item.name}\`${item.package ? ` [${item.package}]` : ""} | confidence ${Math.round(item.confidence * 100)}% | ${item.summary}`);
9
+ }
10
+ lines.push("");
11
+ }
5
12
  lines.push(`- **Project Health:** ${result.score}/100`);
6
13
  lines.push(`- **Path:** ${result.rootDir}`);
7
14
  lines.push(`- **Policy:** ${result.policy.passed ? "PASS" : "FAIL"}`);
@@ -20,16 +27,16 @@ export function renderMarkdownReport(result) {
20
27
  lines.push(`- Outdated: ${result.outdated.length}`);
21
28
  lines.push(`- Risks: ${result.risks.length}`);
22
29
  lines.push("");
23
- appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation)));
30
+ appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation, item.recommendation)));
24
31
  appendSection(lines, "Unused dependencies", result.unused.map((item) => formatEntry(item.package
25
32
  ? `${item.name} (${item.section}) [${item.package}]`
26
- : `${item.name} (${item.section})`, item.confidence, item.explanation)));
33
+ : `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
27
34
  appendSection(lines, "Outdated dependencies", result.outdated.map((item) => formatEntry(item.package
28
35
  ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
29
- : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation)));
36
+ : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
30
37
  appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
31
38
  ? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
32
- : `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation)));
39
+ : `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation, item.recommendation)));
33
40
  appendSection(lines, "Policy reasons", result.policy.reasons);
34
41
  if (result.suggestions.length > 0) {
35
42
  lines.push("## Suggestions");
@@ -50,7 +57,10 @@ function appendSection(lines, title, items) {
50
57
  }
51
58
  lines.push("");
52
59
  }
53
- function formatEntry(label, confidence, explanation) {
60
+ function formatEntry(label, confidence, explanation, recommendation) {
54
61
  const reasonSummary = explanation.length > 0 ? ` | why: ${explanation.join("; ")}` : "";
55
- return `${label} | confidence ${Math.round(confidence * 100)}%${reasonSummary}`;
62
+ const recommendationSummary = recommendation
63
+ ? ` | next: ${recommendation.summary}`
64
+ : "";
65
+ return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
56
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI and library for dependency health analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",