dep-brain 0.5.2 → 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
@@ -4,11 +4,16 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/dep-brain)](https://www.npmjs.com/package/dep-brain)
5
5
  [![license](https://img.shields.io/npm/l/dep-brain)](LICENSE)
6
6
 
7
- `dep-brain` is a CLI and library for analyzing dependency health in JavaScript and TypeScript projects.
7
+ `dep-brain` is a CLI and library for explainable dependency intelligence in JavaScript and TypeScript projects.
8
8
 
9
9
  ## Vision
10
10
 
11
- `npm audit + depcheck + dedupe + intelligence = one tool`
11
+ `dep-brain` aims to become a dependency decision engine:
12
+
13
+ - Explain why a dependency matters
14
+ - Evaluate how safe, risky, or necessary it is
15
+ - Recommend what to do next
16
+ - Enforce decisions in CI workflows
12
17
 
13
18
  ## What It Does
14
19
 
@@ -19,6 +24,12 @@
19
24
  - Generate a simple project health score
20
25
  - Output reports in human-readable or JSON format
21
26
 
27
+ The long-term goal is not just to list problems, but to answer:
28
+
29
+ - Why is this dependency here?
30
+ - Can I remove it safely?
31
+ - What should I fix first?
32
+
22
33
  ## Current MVP Features
23
34
 
24
35
  - Duplicate dependency detection with lockfile instance tracking
@@ -42,6 +53,7 @@ dep-brain analyze
42
53
  npx dep-brain analyze
43
54
  npx dep-brain analyze --json
44
55
  npx dep-brain analyze --md
56
+ npx dep-brain analyze --top
45
57
  npx dep-brain analyze ./path-to-project
46
58
  npx dep-brain analyze --config depbrain.config.json
47
59
  npx dep-brain analyze --min-score 90 --fail-on-risks
@@ -104,6 +116,14 @@ Output includes `outputVersion` for schema stability and can be validated with:
104
116
  dep-brain analyze --md
105
117
  ```
106
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
+
107
127
  ## Report From JSON
108
128
 
109
129
  ```bash
@@ -225,14 +245,20 @@ src/
225
245
  `-- config.ts
226
246
  ```
227
247
 
228
- ## Roadmap Direction
248
+ ## Product Direction
249
+
250
+ `dep-brain` is currently in the `v0.5.x` foundation stage. The next roadmap is:
251
+
252
+ - `v0.6`: explainability and confidence scoring
253
+ - `v0.7`: safe removal guidance and actionable recommendations
254
+ - `v0.8`: supply-chain trust and risk intelligence
255
+ - `v0.9`: deeper monorepo and ownership intelligence
256
+ - `v1.0`: stable CI, ecosystem exports, and production readiness
229
257
 
230
- - Improve false-positive reduction for unused dependency detection
231
- - Improve monorepo and workspace support
232
- - Strengthen risk scoring and suggestions
233
- - Add CI and GitHub Action support in later releases
258
+ The project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
234
259
 
235
260
  ## Repository Notes
236
261
 
237
262
  - Project brief: [docs/project-brief.md](./docs/project-brief.md)
263
+ - Product roadmap: [docs/product-roadmap.md](./docs/product-roadmap.md)
238
264
  - Implementation history: [docs/implementation-log.md](./docs/implementation-log.md)
@@ -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,11 +56,15 @@
44
56
  "type": "array",
45
57
  "items": {
46
58
  "type": "object",
47
- "required": ["name", "versions", "instances"],
59
+ "required": ["name", "versions", "instances", "confidence", "reasonCodes", "explanation", "recommendation"],
48
60
  "additionalProperties": false,
49
61
  "properties": {
50
62
  "name": { "type": "string" },
51
63
  "versions": { "type": "array", "items": { "type": "string" } },
64
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
65
+ "reasonCodes": { "type": "array", "items": { "type": "string" } },
66
+ "explanation": { "type": "array", "items": { "type": "string" } },
67
+ "recommendation": { "$ref": "#/properties/recommendation" },
52
68
  "instances": {
53
69
  "type": "array",
54
70
  "items": {
@@ -68,12 +84,16 @@
68
84
  "type": "array",
69
85
  "items": {
70
86
  "type": "object",
71
- "required": ["name", "section"],
87
+ "required": ["name", "section", "confidence", "reasonCodes", "explanation", "recommendation"],
72
88
  "additionalProperties": false,
73
89
  "properties": {
74
90
  "name": { "type": "string" },
75
91
  "section": { "type": "string", "enum": ["dependencies", "devDependencies"] },
76
- "package": { "type": "string" }
92
+ "package": { "type": "string" },
93
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
94
+ "reasonCodes": { "type": "array", "items": { "type": "string" } },
95
+ "explanation": { "type": "array", "items": { "type": "string" } },
96
+ "recommendation": { "$ref": "#/properties/recommendation" }
77
97
  }
78
98
  }
79
99
  },
@@ -81,14 +101,18 @@
81
101
  "type": "array",
82
102
  "items": {
83
103
  "type": "object",
84
- "required": ["name", "current", "latest", "updateType"],
104
+ "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation", "recommendation"],
85
105
  "additionalProperties": false,
86
106
  "properties": {
87
107
  "name": { "type": "string" },
88
108
  "current": { "type": "string" },
89
109
  "latest": { "type": "string" },
90
110
  "updateType": { "type": "string" },
91
- "package": { "type": "string" }
111
+ "package": { "type": "string" },
112
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
113
+ "reasonCodes": { "type": "array", "items": { "type": "string" } },
114
+ "explanation": { "type": "array", "items": { "type": "string" } },
115
+ "recommendation": { "$ref": "#/properties/recommendation" }
92
116
  }
93
117
  }
94
118
  },
@@ -96,16 +120,37 @@
96
120
  "type": "array",
97
121
  "items": {
98
122
  "type": "object",
99
- "required": ["name", "reasons"],
123
+ "required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "recommendation"],
100
124
  "additionalProperties": false,
101
125
  "properties": {
102
126
  "name": { "type": "string" },
103
127
  "reasons": { "type": "array", "items": { "type": "string" } },
104
- "package": { "type": "string" }
128
+ "package": { "type": "string" },
129
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
130
+ "reasonCodes": { "type": "array", "items": { "type": "string" } },
131
+ "explanation": { "type": "array", "items": { "type": "string" } },
132
+ "recommendation": { "$ref": "#/properties/recommendation" }
105
133
  }
106
134
  }
107
135
  },
108
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
+ },
109
154
  "config": { "type": "object" },
110
155
  "packages": { "type": "array" }
111
156
  }
@@ -3,11 +3,36 @@ export async function findDuplicateDependencies(graph) {
3
3
  .map(([name, instances]) => ({
4
4
  name,
5
5
  versions: Array.from(new Set(instances.map((instance) => instance.version))).sort(),
6
- instances
6
+ instances,
7
+ confidence: 0.98,
8
+ reasonCodes: [
9
+ "multiple_lockfile_versions",
10
+ "multiple_installation_paths"
11
+ ],
12
+ explanation: [
13
+ `Multiple versions of ${name} were found in the lockfile.`,
14
+ "The package is installed from more than one dependency path."
15
+ ],
16
+ recommendation: buildDuplicateRecommendation(Array.from(new Set(instances.map((instance) => instance.version))).sort(), instances.length)
7
17
  }))
8
18
  .filter((dependency) => dependency.versions.length > 1)
9
19
  .sort((left, right) => left.name.localeCompare(right.name));
10
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
+ }
11
36
  export async function runDuplicateCheck(graph) {
12
37
  const duplicates = await findDuplicateDependencies(graph);
13
38
  return {
@@ -17,6 +42,9 @@ export async function runDuplicateCheck(graph) {
17
42
  id: `duplicate:${item.name}`,
18
43
  message: `${item.name} has ${item.versions.length} versions`,
19
44
  severity: "warning",
45
+ confidence: item.confidence,
46
+ reasonCodes: item.reasonCodes,
47
+ explanation: item.explanation,
20
48
  meta: {
21
49
  name: item.name,
22
50
  versions: item.versions,
@@ -11,17 +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,
20
+ confidence: 0.97,
21
+ reasonCodes: [
22
+ "latest_registry_version_newer",
23
+ `update_type_${updateType}`
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)
19
30
  };
20
31
  }));
21
32
  return results
22
33
  .filter((item) => item !== null)
23
34
  .sort((left, right) => left.name.localeCompare(right.name));
24
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
+ }
25
53
  export async function runOutdatedCheck(graph) {
26
54
  const outdated = await findOutdatedDependencies(graph);
27
55
  return {
@@ -31,6 +59,9 @@ export async function runOutdatedCheck(graph) {
31
59
  id: `outdated:${item.name}`,
32
60
  message: `${item.name} ${item.current} -> ${item.latest}`,
33
61
  severity: item.updateType === "major" ? "critical" : "warning",
62
+ confidence: item.confidence,
63
+ reasonCodes: item.reasonCodes,
64
+ explanation: item.explanation,
34
65
  meta: {
35
66
  name: item.name,
36
67
  current: item.current,
@@ -23,12 +23,28 @@ export async function findRiskDependencies(graph) {
23
23
  if (reasons.length === 0) {
24
24
  return null;
25
25
  }
26
- return { name, reasons };
26
+ return {
27
+ name,
28
+ reasons,
29
+ confidence: calculateRiskConfidence(reasons),
30
+ reasonCodes: reasons.map(toRiskReasonCode),
31
+ explanation: reasons,
32
+ recommendation: buildRiskRecommendation(reasons, calculateRiskConfidence(reasons))
33
+ };
27
34
  }));
28
35
  return results
29
36
  .filter((item) => item !== null)
30
37
  .sort((left, right) => left.name.localeCompare(right.name));
31
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
+ }
32
48
  export async function runRiskCheck(graph) {
33
49
  const risks = await findRiskDependencies(graph);
34
50
  return {
@@ -38,6 +54,9 @@ export async function runRiskCheck(graph) {
38
54
  id: `risk:${item.name}`,
39
55
  message: `${item.name}: ${item.reasons.join("; ")}`,
40
56
  severity: "warning",
57
+ confidence: item.confidence,
58
+ reasonCodes: item.reasonCodes,
59
+ explanation: item.explanation,
41
60
  meta: {
42
61
  name: item.name,
43
62
  reasons: item.reasons
@@ -45,3 +64,13 @@ export async function runRiskCheck(graph) {
45
64
  }))
46
65
  };
47
66
  }
67
+ function calculateRiskConfidence(reasons) {
68
+ return Math.min(0.99, 0.55 + reasons.length * 0.12);
69
+ }
70
+ function toRiskReasonCode(reason) {
71
+ const normalized = reason
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9]+/g, "_")
74
+ .replace(/^_+|_+$/g, "");
75
+ return normalized || "risk_signal_detected";
76
+ }
@@ -33,11 +33,11 @@ export async function findUnusedDependencies(rootDir, graph, fileEntries, option
33
33
  }
34
34
  const unusedDependencies = Object.keys(graph.dependencies)
35
35
  .filter((name) => !runtimeUsed.has(name))
36
- .map((name) => ({ name, section: "dependencies" }));
36
+ .map((name) => buildUnusedDependency(name, "dependencies"));
37
37
  const unusedDevDependencies = Object.keys(graph.devDependencies)
38
38
  .filter((name) => !devUsed.has(name) && !runtimeUsed.has(name))
39
39
  .filter((name) => !isImplicitlyUsedDevDependency(name, hasTypeScriptSources, options.hasTypeScriptConfig))
40
- .map((name) => ({ name, section: "devDependencies" }));
40
+ .map((name) => buildUnusedDependency(name, "devDependencies"));
41
41
  return [...unusedDependencies, ...unusedDevDependencies].sort((left, right) => left.name.localeCompare(right.name));
42
42
  }
43
43
  export async function runUnusedCheck(context) {
@@ -49,6 +49,9 @@ export async function runUnusedCheck(context) {
49
49
  id: `unused:${item.section}:${item.name}`,
50
50
  message: `${item.name} appears unused`,
51
51
  severity: "warning",
52
+ confidence: item.confidence,
53
+ reasonCodes: item.reasonCodes,
54
+ explanation: item.explanation,
52
55
  meta: {
53
56
  name: item.name,
54
57
  section: item.section
@@ -127,3 +130,36 @@ function isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScript
127
130
  }
128
131
  return false;
129
132
  }
133
+ function buildUnusedDependency(name, section) {
134
+ return {
135
+ name,
136
+ section,
137
+ confidence: section === "dependencies" ? 0.9 : 0.82,
138
+ reasonCodes: [
139
+ "no_source_import_found",
140
+ "no_config_reference_found",
141
+ "no_script_reference_found"
142
+ ],
143
+ explanation: [
144
+ "No import or require usage was found in scanned source files.",
145
+ "No matching reference was found in recognized config files.",
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."
163
+ ]
164
+ };
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,15 +8,30 @@ 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[];
14
21
  instances: DuplicateInstance[];
22
+ confidence: number;
23
+ reasonCodes: string[];
24
+ explanation: string[];
25
+ recommendation: Recommendation;
15
26
  }
16
27
  export interface UnusedDependency {
17
28
  name: string;
18
29
  section: "dependencies" | "devDependencies";
19
30
  package?: string;
31
+ confidence: number;
32
+ reasonCodes: string[];
33
+ explanation: string[];
34
+ recommendation: Recommendation;
20
35
  }
21
36
  export interface OutdatedDependency {
22
37
  name: string;
@@ -24,11 +39,28 @@ export interface OutdatedDependency {
24
39
  latest: string;
25
40
  updateType: "major" | "minor" | "patch" | "unknown";
26
41
  package?: string;
42
+ confidence: number;
43
+ reasonCodes: string[];
44
+ explanation: string[];
45
+ recommendation: Recommendation;
27
46
  }
28
47
  export interface RiskDependency {
29
48
  name: string;
30
49
  reasons: string[];
31
50
  package?: string;
51
+ confidence: number;
52
+ reasonCodes: string[];
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;
32
64
  }
33
65
  export interface AnalysisResult {
34
66
  outputVersion: string;
@@ -41,6 +73,7 @@ export interface AnalysisResult {
41
73
  outdated: OutdatedDependency[];
42
74
  risks: RiskDependency[];
43
75
  suggestions: string[];
76
+ topIssues: TopIssue[];
44
77
  config: DepBrainConfig;
45
78
  packages?: PackageAnalysisResult[];
46
79
  }
@@ -59,8 +92,9 @@ export interface PackageAnalysisResult {
59
92
  outdated: OutdatedDependency[];
60
93
  risks: RiskDependency[];
61
94
  suggestions: string[];
95
+ topIssues: TopIssue[];
62
96
  }
63
- export declare const OUTPUT_VERSION = "1.0";
97
+ export declare const OUTPUT_VERSION = "1.2";
64
98
  export interface ScoreBreakdown {
65
99
  baseScore: number;
66
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.0";
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
  }
@@ -264,13 +271,21 @@ function mapDuplicateIssues(issues) {
264
271
  versions: Array.isArray(issue.meta?.versions) ? issue.meta?.versions : [],
265
272
  instances: Array.isArray(issue.meta?.instances)
266
273
  ? issue.meta?.instances
267
- : []
274
+ : [],
275
+ confidence: normalizeConfidence(issue.confidence),
276
+ reasonCodes: normalizeStringArray(issue.reasonCodes),
277
+ explanation: normalizeStringArray(issue.explanation),
278
+ recommendation: buildDuplicateRecommendation(issue)
268
279
  }));
269
280
  }
270
281
  function mapUnusedIssues(issues) {
271
282
  return issues.map((issue) => ({
272
283
  name: String(issue.meta?.name ?? issue.package ?? "unknown"),
273
- section: issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies"
284
+ section: issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies",
285
+ confidence: normalizeConfidence(issue.confidence),
286
+ reasonCodes: normalizeStringArray(issue.reasonCodes),
287
+ explanation: normalizeStringArray(issue.explanation),
288
+ recommendation: buildUnusedRecommendation(issue)
274
289
  }));
275
290
  }
276
291
  function mapOutdatedIssues(issues) {
@@ -280,15 +295,148 @@ function mapOutdatedIssues(issues) {
280
295
  latest: String(issue.meta?.latest ?? ""),
281
296
  updateType: issue.meta?.updateType === "major" || issue.meta?.updateType === "minor" || issue.meta?.updateType === "patch"
282
297
  ? issue.meta.updateType
283
- : "unknown"
298
+ : "unknown",
299
+ confidence: normalizeConfidence(issue.confidence),
300
+ reasonCodes: normalizeStringArray(issue.reasonCodes),
301
+ explanation: normalizeStringArray(issue.explanation),
302
+ recommendation: buildOutdatedRecommendation(issue)
284
303
  }));
285
304
  }
286
305
  function mapRiskIssues(issues) {
287
306
  return issues.map((issue) => ({
288
307
  name: String(issue.meta?.name ?? issue.package ?? "unknown"),
289
- reasons: Array.isArray(issue.meta?.reasons) ? issue.meta?.reasons : []
308
+ reasons: Array.isArray(issue.meta?.reasons) ? issue.meta?.reasons : [],
309
+ confidence: normalizeConfidence(issue.confidence),
310
+ reasonCodes: normalizeStringArray(issue.reasonCodes),
311
+ explanation: normalizeStringArray(issue.explanation),
312
+ recommendation: buildRiskRecommendation(issue)
290
313
  }));
291
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
+ }
428
+ function normalizeConfidence(value) {
429
+ if (typeof value !== "number" || Number.isNaN(value)) {
430
+ return 0.5;
431
+ }
432
+ return Math.min(0.99, Math.max(0, Number(value.toFixed(2))));
433
+ }
434
+ function normalizeStringArray(value) {
435
+ if (!Array.isArray(value)) {
436
+ return [];
437
+ }
438
+ return value.filter((entry) => typeof entry === "string");
439
+ }
292
440
  function buildScoreBreakdown(counts, config) {
293
441
  return {
294
442
  baseScore: 100,
@@ -1,9 +1,13 @@
1
1
  export type IssueSeverity = "info" | "warning" | "critical";
2
+ export type ReasonCode = string;
2
3
  export type Issue = {
3
4
  id: string;
4
5
  message: string;
5
6
  package?: string;
6
7
  severity: IssueSeverity;
8
+ confidence?: number;
9
+ reasonCodes?: ReasonCode[];
10
+ explanation?: string[];
7
11
  meta?: Record<string, unknown>;
8
12
  };
9
13
  export type CheckResult = {
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) => `${item.name}: ${item.versions.join(", ")}`));
20
- appendSection(lines, "Unused dependencies", result.unused.map((item) => item.package
26
+ appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation, item.recommendation)));
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})`));
23
- appendSection(lines, "Outdated dependencies", result.outdated.map((item) => item.package
29
+ : `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
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}]`));
26
- appendSection(lines, "Risky dependencies", result.risks.map((item) => item.package
32
+ : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
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("; ")}`));
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,3 +57,10 @@ function appendSection(lines, title, entries) {
50
57
  lines.push(`- ${entry}`);
51
58
  }
52
59
  }
60
+ function formatEntry(label, confidence, explanation, recommendation) {
61
+ const reasonSummary = explanation.length > 0 ? ` | why: ${explanation.join("; ")}` : "";
62
+ const recommendationSummary = recommendation
63
+ ? ` | next: ${recommendation.summary}`
64
+ : "";
65
+ return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
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) => `${item.name}: ${item.versions.join(", ")}`));
24
- appendSection(lines, "Unused dependencies", result.unused.map((item) => item.package
30
+ appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation, item.recommendation)));
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})`));
27
- appendSection(lines, "Outdated dependencies", result.outdated.map((item) => item.package
33
+ : `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
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}]`));
30
- appendSection(lines, "Risky dependencies", result.risks.map((item) => item.package
36
+ : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
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("; ")}`));
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,3 +57,10 @@ function appendSection(lines, title, items) {
50
57
  }
51
58
  lines.push("");
52
59
  }
60
+ function formatEntry(label, confidence, explanation, recommendation) {
61
+ const reasonSummary = explanation.length > 0 ? ` | why: ${explanation.join("; ")}` : "";
62
+ const recommendationSummary = recommendation
63
+ ? ` | next: ${recommendation.summary}`
64
+ : "";
65
+ return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "0.5.2",
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",