dep-brain 0.6.0 → 0.8.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,7 @@ 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`.
12
+ - Supply-chain trust scoring for risk findings.
13
+ - Structured risk factors in JSON output.
package/README.md CHANGED
@@ -21,6 +21,7 @@
21
21
  - Detect likely unused dependencies from source imports and scripts
22
22
  - Detect outdated packages
23
23
  - Highlight dependency risk signals
24
+ - Score package trust using supply-chain metadata
24
25
  - Generate a simple project health score
25
26
  - Output reports in human-readable or JSON format
26
27
 
@@ -53,6 +54,7 @@ dep-brain analyze
53
54
  npx dep-brain analyze
54
55
  npx dep-brain analyze --json
55
56
  npx dep-brain analyze --md
57
+ npx dep-brain analyze --top
56
58
  npx dep-brain analyze ./path-to-project
57
59
  npx dep-brain analyze --config depbrain.config.json
58
60
  npx dep-brain analyze --min-score 90 --fail-on-risks
@@ -115,6 +117,14 @@ Output includes `outputVersion` for schema stability and can be validated with:
115
117
  dep-brain analyze --md
116
118
  ```
117
119
 
120
+ ## Top Issues Output
121
+
122
+ ```bash
123
+ dep-brain analyze --top
124
+ ```
125
+
126
+ Shows the highest-priority actionable findings first, including confidence and next-step guidance.
127
+
118
128
  ## Report From JSON
119
129
 
120
130
  ```bash
@@ -248,6 +258,8 @@ src/
248
258
 
249
259
  The project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
250
260
 
261
+ Risk findings now include a `trustScore` plus structured `riskFactors` such as publish recency, maintainer count, and repository presence.
262
+
251
263
  ## Repository Notes
252
264
 
253
265
  - Project brief: [docs/project-brief.md](./docs/project-brief.md)
@@ -2,9 +2,35 @@
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
+ },
20
+ "riskFactors": {
21
+ "type": "object",
22
+ "required": ["daysSincePublish", "downloads", "maintainersCount", "versionCount", "recentReleaseCount", "hasRepository", "dependencyType"],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "daysSincePublish": { "type": ["number", "null"] },
26
+ "downloads": { "type": ["number", "null"] },
27
+ "maintainersCount": { "type": ["number", "null"] },
28
+ "versionCount": { "type": ["number", "null"] },
29
+ "recentReleaseCount": { "type": ["number", "null"] },
30
+ "hasRepository": { "type": "boolean" },
31
+ "dependencyType": { "type": "string", "enum": ["dependencies", "devDependencies", "unknown"] }
32
+ }
33
+ },
8
34
  "outputVersion": { "type": "string" },
9
35
  "rootDir": { "type": "string" },
10
36
  "score": { "type": "number" },
@@ -44,7 +70,7 @@
44
70
  "type": "array",
45
71
  "items": {
46
72
  "type": "object",
47
- "required": ["name", "versions", "instances", "confidence", "reasonCodes", "explanation"],
73
+ "required": ["name", "versions", "instances", "confidence", "reasonCodes", "explanation", "recommendation"],
48
74
  "additionalProperties": false,
49
75
  "properties": {
50
76
  "name": { "type": "string" },
@@ -52,6 +78,7 @@
52
78
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
53
79
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
54
80
  "explanation": { "type": "array", "items": { "type": "string" } },
81
+ "recommendation": { "$ref": "#/properties/recommendation" },
55
82
  "instances": {
56
83
  "type": "array",
57
84
  "items": {
@@ -71,7 +98,7 @@
71
98
  "type": "array",
72
99
  "items": {
73
100
  "type": "object",
74
- "required": ["name", "section", "confidence", "reasonCodes", "explanation"],
101
+ "required": ["name", "section", "confidence", "reasonCodes", "explanation", "recommendation"],
75
102
  "additionalProperties": false,
76
103
  "properties": {
77
104
  "name": { "type": "string" },
@@ -79,7 +106,8 @@
79
106
  "package": { "type": "string" },
80
107
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
81
108
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
82
- "explanation": { "type": "array", "items": { "type": "string" } }
109
+ "explanation": { "type": "array", "items": { "type": "string" } },
110
+ "recommendation": { "$ref": "#/properties/recommendation" }
83
111
  }
84
112
  }
85
113
  },
@@ -87,7 +115,7 @@
87
115
  "type": "array",
88
116
  "items": {
89
117
  "type": "object",
90
- "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation"],
118
+ "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation", "recommendation"],
91
119
  "additionalProperties": false,
92
120
  "properties": {
93
121
  "name": { "type": "string" },
@@ -97,7 +125,8 @@
97
125
  "package": { "type": "string" },
98
126
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
99
127
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
100
- "explanation": { "type": "array", "items": { "type": "string" } }
128
+ "explanation": { "type": "array", "items": { "type": "string" } },
129
+ "recommendation": { "$ref": "#/properties/recommendation" }
101
130
  }
102
131
  }
103
132
  },
@@ -105,7 +134,7 @@
105
134
  "type": "array",
106
135
  "items": {
107
136
  "type": "object",
108
- "required": ["name", "reasons", "confidence", "reasonCodes", "explanation"],
137
+ "required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "trustScore", "riskFactors", "recommendation"],
109
138
  "additionalProperties": false,
110
139
  "properties": {
111
140
  "name": { "type": "string" },
@@ -113,11 +142,32 @@
113
142
  "package": { "type": "string" },
114
143
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
115
144
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
116
- "explanation": { "type": "array", "items": { "type": "string" } }
145
+ "explanation": { "type": "array", "items": { "type": "string" } },
146
+ "trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
147
+ "riskFactors": { "$ref": "#/properties/riskFactors" },
148
+ "recommendation": { "$ref": "#/properties/recommendation" }
117
149
  }
118
150
  }
119
151
  },
120
152
  "suggestions": { "type": "array", "items": { "type": "string" } },
153
+ "topIssues": {
154
+ "type": "array",
155
+ "items": {
156
+ "type": "object",
157
+ "required": ["kind", "name", "priority", "confidence", "summary", "recommendation"],
158
+ "additionalProperties": false,
159
+ "properties": {
160
+ "kind": { "type": "string", "enum": ["unused", "duplicate", "outdated", "risk"] },
161
+ "name": { "type": "string" },
162
+ "package": { "type": "string" },
163
+ "priority": { "type": "string", "enum": ["high", "medium", "low"] },
164
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
165
+ "summary": { "type": "string" },
166
+ "trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
167
+ "recommendation": { "$ref": "#/properties/recommendation" }
168
+ }
169
+ }
170
+ },
121
171
  "config": { "type": "object" },
122
172
  "packages": { "type": "array" }
123
173
  }
@@ -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 {
@@ -1,5 +1,9 @@
1
1
  import type { DependencyGraph } from "../core/graph-builder.js";
2
2
  import type { RiskDependency } from "../core/analyzer.js";
3
3
  import type { CheckResult } from "../core/types.js";
4
- export declare function findRiskDependencies(graph: DependencyGraph): Promise<RiskDependency[]>;
4
+ import { type PackageMetadata } from "../utils/npm-api.js";
5
+ export interface RiskCheckOptions {
6
+ resolvePackageMetadata?: (name: string) => Promise<PackageMetadata | null>;
7
+ }
8
+ export declare function findRiskDependencies(graph: DependencyGraph, options?: RiskCheckOptions): Promise<RiskDependency[]>;
5
9
  export declare function runRiskCheck(graph: DependencyGraph): Promise<CheckResult>;
@@ -1,34 +1,35 @@
1
1
  import { getPackageMetadata } from "../utils/npm-api.js";
2
2
  const TWO_YEARS_IN_DAYS = 365 * 2;
3
- export async function findRiskDependencies(graph) {
3
+ const ONE_YEAR_IN_DAYS = 365;
4
+ export async function findRiskDependencies(graph, options = {}) {
5
+ const resolvePackageMetadata = options.resolvePackageMetadata ?? getPackageMetadata;
4
6
  const names = Object.keys({
5
7
  ...graph.dependencies,
6
8
  ...graph.devDependencies
7
9
  });
8
10
  const results = await Promise.all(names.map(async (name) => {
9
- const metadata = await getPackageMetadata(name);
10
- const reasons = [];
11
+ const metadata = await resolvePackageMetadata(name);
11
12
  if (!metadata) {
12
13
  return null;
13
14
  }
14
- if (metadata.daysSincePublish !== null && metadata.daysSincePublish > TWO_YEARS_IN_DAYS) {
15
- reasons.push("No release in over 2 years");
16
- }
17
- if (metadata.downloads !== null && metadata.downloads < 1000) {
18
- reasons.push("Low weekly download volume");
19
- }
20
- if (!metadata.repository) {
21
- reasons.push("Missing repository metadata");
22
- }
23
- if (reasons.length === 0) {
15
+ const dependencyType = graph.dependencies[name]
16
+ ? "dependencies"
17
+ : graph.devDependencies[name]
18
+ ? "devDependencies"
19
+ : "unknown";
20
+ const assessment = assessRisk(metadata, dependencyType);
21
+ if (assessment.reasons.length === 0) {
24
22
  return null;
25
23
  }
26
24
  return {
27
25
  name,
28
- reasons,
29
- confidence: calculateRiskConfidence(reasons),
30
- reasonCodes: reasons.map(toRiskReasonCode),
31
- explanation: reasons
26
+ reasons: assessment.reasons,
27
+ confidence: assessment.confidence,
28
+ reasonCodes: assessment.reasonCodes,
29
+ explanation: assessment.reasons,
30
+ trustScore: assessment.trustScore,
31
+ riskFactors: assessment.riskFactors,
32
+ recommendation: buildRiskRecommendation(assessment.reasons, assessment.confidence, assessment.trustScore)
32
33
  };
33
34
  }));
34
35
  return results
@@ -43,24 +44,89 @@ export async function runRiskCheck(graph) {
43
44
  issues: risks.map((item) => ({
44
45
  id: `risk:${item.name}`,
45
46
  message: `${item.name}: ${item.reasons.join("; ")}`,
46
- severity: "warning",
47
+ severity: item.trustScore === "low"
48
+ ? "critical"
49
+ : item.trustScore === "medium"
50
+ ? "warning"
51
+ : "info",
47
52
  confidence: item.confidence,
48
53
  reasonCodes: item.reasonCodes,
49
54
  explanation: item.explanation,
50
55
  meta: {
51
56
  name: item.name,
52
- reasons: item.reasons
57
+ reasons: item.reasons,
58
+ trustScore: item.trustScore,
59
+ riskFactors: item.riskFactors
53
60
  }
54
61
  }))
55
62
  };
56
63
  }
57
- function calculateRiskConfidence(reasons) {
58
- return Math.min(0.99, 0.55 + reasons.length * 0.12);
64
+ function assessRisk(metadata, dependencyType) {
65
+ const reasons = [];
66
+ const reasonCodes = [];
67
+ let weight = 0;
68
+ if (metadata.daysSincePublish !== null && metadata.daysSincePublish > TWO_YEARS_IN_DAYS) {
69
+ reasons.push("No release in over 2 years");
70
+ reasonCodes.push("stale_release");
71
+ weight += 3;
72
+ }
73
+ else if (metadata.daysSincePublish !== null &&
74
+ metadata.daysSincePublish > ONE_YEAR_IN_DAYS) {
75
+ reasons.push("No release in over 12 months");
76
+ reasonCodes.push("aging_release");
77
+ weight += 2;
78
+ }
79
+ if (metadata.downloads !== null && metadata.downloads < 1000) {
80
+ reasons.push("Low weekly download volume");
81
+ reasonCodes.push("low_download_volume");
82
+ weight += 2;
83
+ }
84
+ if (!metadata.repository) {
85
+ reasons.push("Missing repository metadata");
86
+ reasonCodes.push("missing_repository_metadata");
87
+ weight += 2;
88
+ }
89
+ if (metadata.maintainersCount !== null && metadata.maintainersCount <= 1) {
90
+ reasons.push("Single maintainer package");
91
+ reasonCodes.push("single_maintainer");
92
+ weight += 2;
93
+ }
94
+ if (metadata.recentReleaseCount !== null && metadata.recentReleaseCount === 0) {
95
+ reasons.push("No releases published in the last 30 days");
96
+ reasonCodes.push("no_recent_release");
97
+ weight += 1;
98
+ }
99
+ if (metadata.versionCount !== null && metadata.versionCount <= 3) {
100
+ reasons.push("Very limited published version history");
101
+ reasonCodes.push("limited_version_history");
102
+ weight += 1;
103
+ }
104
+ const confidence = reasons.length === 0 ? 0.5 : Math.min(0.99, 0.52 + weight * 0.07);
105
+ const trustScore = weight >= 6 ? "low" : weight >= 3 ? "medium" : "high";
106
+ return {
107
+ confidence,
108
+ trustScore,
109
+ reasons,
110
+ reasonCodes,
111
+ riskFactors: {
112
+ daysSincePublish: metadata.daysSincePublish,
113
+ downloads: metadata.downloads,
114
+ maintainersCount: metadata.maintainersCount,
115
+ versionCount: metadata.versionCount,
116
+ recentReleaseCount: metadata.recentReleaseCount,
117
+ hasRepository: Boolean(metadata.repository),
118
+ dependencyType
119
+ }
120
+ };
59
121
  }
60
- function toRiskReasonCode(reason) {
61
- const normalized = reason
62
- .toLowerCase()
63
- .replace(/[^a-z0-9]+/g, "_")
64
- .replace(/^_+|_+$/g, "");
65
- return normalized || "risk_signal_detected";
122
+ function buildRiskRecommendation(reasons, confidence, trustScore) {
123
+ return {
124
+ action: "review",
125
+ priority: trustScore === "low" || confidence >= 0.8 ? "high" : "medium",
126
+ safety: "caution",
127
+ summary: trustScore === "low"
128
+ ? "Low trust package; review whether to replace, pin, or monitor it closely."
129
+ : "Review package trust signals and decide whether to keep, replace, or monitor it.",
130
+ reasons
131
+ };
66
132
  }
@@ -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,23 @@ 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
+ }
18
+ export interface RiskFactors {
19
+ daysSincePublish: number | null;
20
+ downloads: number | null;
21
+ maintainersCount: number | null;
22
+ versionCount: number | null;
23
+ recentReleaseCount: number | null;
24
+ hasRepository: boolean;
25
+ dependencyType: "dependencies" | "devDependencies" | "unknown";
26
+ }
27
+ export type TrustScore = "high" | "medium" | "low";
11
28
  export interface DuplicateDependency {
12
29
  name: string;
13
30
  versions: string[];
@@ -15,6 +32,7 @@ export interface DuplicateDependency {
15
32
  confidence: number;
16
33
  reasonCodes: string[];
17
34
  explanation: string[];
35
+ recommendation: Recommendation;
18
36
  }
19
37
  export interface UnusedDependency {
20
38
  name: string;
@@ -23,6 +41,7 @@ export interface UnusedDependency {
23
41
  confidence: number;
24
42
  reasonCodes: string[];
25
43
  explanation: string[];
44
+ recommendation: Recommendation;
26
45
  }
27
46
  export interface OutdatedDependency {
28
47
  name: string;
@@ -33,6 +52,7 @@ export interface OutdatedDependency {
33
52
  confidence: number;
34
53
  reasonCodes: string[];
35
54
  explanation: string[];
55
+ recommendation: Recommendation;
36
56
  }
37
57
  export interface RiskDependency {
38
58
  name: string;
@@ -41,6 +61,19 @@ export interface RiskDependency {
41
61
  confidence: number;
42
62
  reasonCodes: string[];
43
63
  explanation: string[];
64
+ trustScore: TrustScore;
65
+ riskFactors: RiskFactors;
66
+ recommendation: Recommendation;
67
+ }
68
+ export interface TopIssue {
69
+ kind: "unused" | "duplicate" | "outdated" | "risk";
70
+ name: string;
71
+ package?: string;
72
+ priority: "high" | "medium" | "low";
73
+ confidence: number;
74
+ summary: string;
75
+ trustScore?: TrustScore;
76
+ recommendation: Recommendation;
44
77
  }
45
78
  export interface AnalysisResult {
46
79
  outputVersion: string;
@@ -53,6 +86,7 @@ export interface AnalysisResult {
53
86
  outdated: OutdatedDependency[];
54
87
  risks: RiskDependency[];
55
88
  suggestions: string[];
89
+ topIssues: TopIssue[];
56
90
  config: DepBrainConfig;
57
91
  packages?: PackageAnalysisResult[];
58
92
  }
@@ -71,8 +105,9 @@ export interface PackageAnalysisResult {
71
105
  outdated: OutdatedDependency[];
72
106
  risks: RiskDependency[];
73
107
  suggestions: string[];
108
+ topIssues: TopIssue[];
74
109
  }
75
- export declare const OUTPUT_VERSION = "1.1";
110
+ export declare const OUTPUT_VERSION = "1.3";
76
111
  export interface ScoreBreakdown {
77
112
  baseScore: number;
78
113
  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.3";
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,135 @@ 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
+ trustScore: normalizeTrustScore(issue.meta?.trustScore),
313
+ riskFactors: normalizeRiskFactors(issue.meta?.riskFactors),
314
+ recommendation: buildRiskRecommendation(issue)
302
315
  }));
303
316
  }
317
+ function buildUnusedRecommendation(issue) {
318
+ const confidence = normalizeConfidence(issue.confidence);
319
+ const section = issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies";
320
+ const safety = section === "devDependencies" || confidence >= 0.88
321
+ ? "safe"
322
+ : confidence >= 0.7
323
+ ? "caution"
324
+ : "unknown";
325
+ return {
326
+ action: "remove",
327
+ priority: confidence >= 0.88 ? "high" : "medium",
328
+ safety,
329
+ summary: safety === "safe"
330
+ ? `Safe to remove from ${section}.`
331
+ : safety === "caution"
332
+ ? `Likely removable from ${section}, but review before deleting.`
333
+ : `Potentially removable from ${section}, but evidence is limited.`,
334
+ reasons: normalizeStringArray(issue.explanation)
335
+ };
336
+ }
337
+ function buildDuplicateRecommendation(issue) {
338
+ const versions = Array.isArray(issue.meta?.versions) ? issue.meta.versions : [];
339
+ const targetVersion = versions[versions.length - 1];
340
+ const instances = Array.isArray(issue.meta?.instances) ? issue.meta.instances.length : 0;
341
+ return {
342
+ action: "consolidate",
343
+ priority: versions.length >= 3 ? "high" : "medium",
344
+ safety: "caution",
345
+ summary: targetVersion
346
+ ? `Consolidate toward ${targetVersion}; ${instances} installation paths are affected.`
347
+ : "Consolidate duplicate versions to a single target version.",
348
+ reasons: normalizeStringArray(issue.explanation)
349
+ };
350
+ }
351
+ function buildOutdatedRecommendation(issue) {
352
+ const updateType = issue.meta?.updateType === "major" ||
353
+ issue.meta?.updateType === "minor" ||
354
+ issue.meta?.updateType === "patch"
355
+ ? issue.meta.updateType
356
+ : "unknown";
357
+ const priority = updateType === "major" ? "high" : updateType === "minor" ? "medium" : "low";
358
+ const safety = updateType === "patch" ? "safe" : updateType === "minor" ? "caution" : "unknown";
359
+ return {
360
+ action: "upgrade",
361
+ priority,
362
+ safety,
363
+ summary: updateType === "major"
364
+ ? "New major version available; review breaking changes before upgrading."
365
+ : updateType === "minor"
366
+ ? "New minor version available; review release notes before upgrading."
367
+ : updateType === "patch"
368
+ ? "Routine patch update available."
369
+ : "Newer version available; review upgrade impact.",
370
+ reasons: normalizeStringArray(issue.explanation)
371
+ };
372
+ }
373
+ function buildRiskRecommendation(issue) {
374
+ const reasons = normalizeStringArray(issue.explanation);
375
+ const confidence = normalizeConfidence(issue.confidence);
376
+ const trustScore = normalizeTrustScore(issue.meta?.trustScore);
377
+ return {
378
+ action: "review",
379
+ priority: trustScore === "low" || confidence >= 0.79 ? "high" : "medium",
380
+ safety: "caution",
381
+ summary: trustScore === "low"
382
+ ? "Low trust package; review whether to replace, pin, or monitor it closely."
383
+ : "Review package trust signals and decide whether to keep, replace, or monitor it.",
384
+ reasons
385
+ };
386
+ }
387
+ function buildTopIssues(input) {
388
+ const issues = [
389
+ ...input.unused.map((item) => ({
390
+ kind: "unused",
391
+ name: item.name,
392
+ package: item.package,
393
+ priority: item.recommendation.priority,
394
+ confidence: item.confidence,
395
+ summary: item.recommendation.summary,
396
+ recommendation: item.recommendation
397
+ })),
398
+ ...input.duplicates.map((item) => ({
399
+ kind: "duplicate",
400
+ name: item.name,
401
+ priority: item.recommendation.priority,
402
+ confidence: item.confidence,
403
+ summary: item.recommendation.summary,
404
+ recommendation: item.recommendation
405
+ })),
406
+ ...input.outdated.map((item) => ({
407
+ kind: "outdated",
408
+ name: item.name,
409
+ package: item.package,
410
+ priority: item.recommendation.priority,
411
+ confidence: item.confidence,
412
+ summary: item.recommendation.summary,
413
+ recommendation: item.recommendation
414
+ })),
415
+ ...input.risks.map((item) => ({
416
+ kind: "risk",
417
+ name: item.name,
418
+ package: item.package,
419
+ priority: item.recommendation.priority,
420
+ confidence: item.confidence,
421
+ summary: item.recommendation.summary,
422
+ trustScore: item.trustScore,
423
+ recommendation: item.recommendation
424
+ }))
425
+ ];
426
+ return issues
427
+ .sort((left, right) => comparePriority(right.priority, left.priority) ||
428
+ compareTrustScore(right.trustScore, left.trustScore) ||
429
+ right.confidence - left.confidence)
430
+ .slice(0, 5);
431
+ }
432
+ function comparePriority(left, right) {
433
+ const rank = { high: 3, medium: 2, low: 1 };
434
+ return rank[left] - rank[right];
435
+ }
436
+ function compareTrustScore(left, right) {
437
+ const rank = { low: 3, medium: 2, high: 1, undefined: 0 };
438
+ return rank[left ?? "undefined"] - rank[right ?? "undefined"];
439
+ }
304
440
  function normalizeConfidence(value) {
305
441
  if (typeof value !== "number" || Number.isNaN(value)) {
306
442
  return 0.5;
@@ -313,6 +449,36 @@ function normalizeStringArray(value) {
313
449
  }
314
450
  return value.filter((entry) => typeof entry === "string");
315
451
  }
452
+ function normalizeTrustScore(value) {
453
+ return value === "high" || value === "medium" || value === "low"
454
+ ? value
455
+ : "medium";
456
+ }
457
+ function normalizeRiskFactors(value) {
458
+ if (!value || typeof value !== "object") {
459
+ return {
460
+ daysSincePublish: null,
461
+ downloads: null,
462
+ maintainersCount: null,
463
+ versionCount: null,
464
+ recentReleaseCount: null,
465
+ hasRepository: false,
466
+ dependencyType: "unknown"
467
+ };
468
+ }
469
+ const factors = value;
470
+ return {
471
+ daysSincePublish: typeof factors.daysSincePublish === "number" ? factors.daysSincePublish : null,
472
+ downloads: typeof factors.downloads === "number" ? factors.downloads : null,
473
+ maintainersCount: typeof factors.maintainersCount === "number" ? factors.maintainersCount : null,
474
+ versionCount: typeof factors.versionCount === "number" ? factors.versionCount : null,
475
+ recentReleaseCount: typeof factors.recentReleaseCount === "number" ? factors.recentReleaseCount : null,
476
+ hasRepository: factors.hasRepository === true,
477
+ dependencyType: factors.dependencyType === "dependencies" || factors.dependencyType === "devDependencies"
478
+ ? factors.dependencyType
479
+ : "unknown"
480
+ };
481
+ }
316
482
  function buildScoreBreakdown(counts, config) {
317
483
  return {
318
484
  baseScore: 100,
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}]` : ""}${item.trustScore ? ` | trust ${item.trustScore.toUpperCase()}` : ""} | 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
- ? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
28
- : `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation)));
34
+ ? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]`
35
+ : `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]`, 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}]` : ""}${item.trustScore ? ` | trust ${item.trustScore.toUpperCase()}` : ""} | 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
- ? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
32
- : `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation)));
38
+ ? `${item.name}: ${item.reasons.join("; ")} [${item.package}] [trust ${item.trustScore.toUpperCase()}]`
39
+ : `${item.name}: ${item.reasons.join("; ")} [trust ${item.trustScore.toUpperCase()}]`, 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
  }
@@ -3,6 +3,9 @@ export interface PackageMetadata {
3
3
  repository: string | null;
4
4
  downloads: number | null;
5
5
  daysSincePublish: number | null;
6
+ maintainersCount: number | null;
7
+ versionCount: number | null;
8
+ recentReleaseCount: number | null;
6
9
  }
7
10
  export declare function getLatestVersion(name: string): Promise<string | null>;
8
11
  export declare function getPackageMetadata(name: string): Promise<PackageMetadata | null>;
@@ -39,14 +39,35 @@ async function fetchPackageMetadata(name) {
39
39
  const repository = typeof packageJson.repository === "string"
40
40
  ? packageJson.repository
41
41
  : packageJson.repository?.url ?? null;
42
+ const maintainersCount = Array.isArray(packageJson.maintainers)
43
+ ? packageJson.maintainers.length
44
+ : null;
45
+ const versionCount = packageJson.versions
46
+ ? Object.keys(packageJson.versions).length
47
+ : null;
48
+ const recentReleaseCount = countRecentReleases(packageJson.time ?? {});
42
49
  return {
43
50
  latestVersion,
44
51
  repository,
45
52
  downloads: downloadsJson.downloads ?? null,
46
- daysSincePublish
53
+ daysSincePublish,
54
+ maintainersCount,
55
+ versionCount,
56
+ recentReleaseCount
47
57
  };
48
58
  }
49
59
  catch {
50
60
  return null;
51
61
  }
52
62
  }
63
+ function countRecentReleases(time) {
64
+ const values = Object.entries(time)
65
+ .filter(([key]) => key !== "created" && key !== "modified")
66
+ .map(([, value]) => new Date(value).getTime())
67
+ .filter((value) => Number.isFinite(value));
68
+ if (values.length === 0) {
69
+ return null;
70
+ }
71
+ const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
72
+ return values.filter((value) => value >= thirtyDaysAgo).length;
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI and library for dependency health analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",