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 +2 -0
- package/README.md +9 -0
- package/depbrain.output.schema.json +41 -8
- package/dist/checks/duplicate.js +17 -1
- package/dist/checks/outdated.js +23 -4
- package/dist/checks/risk.js +11 -1
- package/dist/checks/unused.js +16 -0
- package/dist/cli.js +26 -5
- package/dist/core/analyzer.d.ts +23 -1
- package/dist/core/analyzer.js +129 -5
- package/dist/index.d.ts +1 -1
- package/dist/reporters/console.js +16 -6
- package/dist/reporters/markdown.js +16 -6
- package/package.json +1 -1
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
|
}
|
package/dist/checks/duplicate.js
CHANGED
|
@@ -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 {
|
package/dist/checks/outdated.js
CHANGED
|
@@ -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
|
|
19
|
+
updateType,
|
|
19
20
|
confidence: 0.97,
|
|
20
21
|
reasonCodes: [
|
|
21
22
|
"latest_registry_version_newer",
|
|
22
|
-
`update_type_${
|
|
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 ${
|
|
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 {
|
package/dist/checks/risk.js
CHANGED
|
@@ -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 {
|
package/dist/checks/unused.js
CHANGED
|
@@ -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("--
|
|
55
|
-
?
|
|
56
|
-
:
|
|
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
|
+
}
|
package/dist/core/analyzer.d.ts
CHANGED
|
@@ -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.
|
|
97
|
+
export declare const OUTPUT_VERSION = "1.2";
|
|
76
98
|
export interface ScoreBreakdown {
|
|
77
99
|
baseScore: number;
|
|
78
100
|
duplicates: number;
|
package/dist/core/analyzer.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
const recommendationSummary = recommendation
|
|
63
|
+
? ` | next: ${recommendation.summary}`
|
|
64
|
+
: "";
|
|
65
|
+
return `${label} | confidence ${Math.round(confidence * 100)}%${recommendationSummary}${reasonSummary}`;
|
|
56
66
|
}
|