dep-brain 0.7.0 → 0.9.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 +5 -1
- package/README.md +9 -0
- package/depbrain.output.schema.json +46 -3
- package/dist/checks/duplicate.js +2 -0
- package/dist/checks/risk.d.ts +5 -1
- package/dist/checks/risk.js +93 -37
- package/dist/core/analyzer.d.ts +29 -1
- package/dist/core/analyzer.js +132 -7
- package/dist/index.d.ts +1 -1
- package/dist/reporters/console.js +5 -5
- package/dist/reporters/markdown.js +5 -5
- package/dist/utils/npm-api.d.ts +3 -0
- package/dist/utils/npm-api.js +22 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 0.9.0
|
|
6
6
|
|
|
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
10
|
- Actionable recommendations for unused, duplicate, outdated, and risk findings.
|
|
11
11
|
- Ranked top-issues summary output with `--top`.
|
|
12
|
+
- Supply-chain trust scoring for risk findings.
|
|
13
|
+
- Structured risk factors in JSON output.
|
|
14
|
+
- Monorepo ownership summaries for workspace packages.
|
|
15
|
+
- Workspace-level duplicate attribution and root-cause tracing.
|
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
|
|
|
@@ -74,6 +75,12 @@ dep-brain --version
|
|
|
74
75
|
|
|
75
76
|
If the root `package.json` defines `workspaces`, `dep-brain` analyzes each workspace package and reports per-package results. Aggregated counts are still shown at the top-level summary.
|
|
76
77
|
|
|
78
|
+
Workspace analysis now includes:
|
|
79
|
+
|
|
80
|
+
- per-workspace ownership summaries
|
|
81
|
+
- root-level duplicate attribution back to contributing workspaces
|
|
82
|
+
- top issues that stay tagged to the workspace that should act
|
|
83
|
+
|
|
77
84
|
## Example Output
|
|
78
85
|
|
|
79
86
|
```text
|
|
@@ -257,6 +264,8 @@ src/
|
|
|
257
264
|
|
|
258
265
|
The project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
|
|
259
266
|
|
|
267
|
+
Risk findings now include a `trustScore` plus structured `riskFactors` such as publish recency, maintainer count, and repository presence.
|
|
268
|
+
|
|
260
269
|
## Repository Notes
|
|
261
270
|
|
|
262
271
|
- Project brief: [docs/project-brief.md](./docs/project-brief.md)
|
|
@@ -2,7 +2,7 @@
|
|
|
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", "topIssues", "config"],
|
|
5
|
+
"required": ["outputVersion", "rootDir", "score", "scoreBreakdown", "policy", "ownershipSummary", "duplicates", "unused", "outdated", "risks", "suggestions", "topIssues", "config"],
|
|
6
6
|
"additionalProperties": false,
|
|
7
7
|
"properties": {
|
|
8
8
|
"recommendation": {
|
|
@@ -17,6 +17,31 @@
|
|
|
17
17
|
"reasons": { "type": "array", "items": { "type": "string" } }
|
|
18
18
|
}
|
|
19
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
|
+
},
|
|
34
|
+
"ownershipSummary": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"required": ["duplicates", "unused", "outdated", "risks"],
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"properties": {
|
|
39
|
+
"duplicates": { "type": "number" },
|
|
40
|
+
"unused": { "type": "number" },
|
|
41
|
+
"outdated": { "type": "number" },
|
|
42
|
+
"risks": { "type": "number" }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
20
45
|
"outputVersion": { "type": "string" },
|
|
21
46
|
"rootDir": { "type": "string" },
|
|
22
47
|
"score": { "type": "number" },
|
|
@@ -52,15 +77,30 @@
|
|
|
52
77
|
"reasons": { "type": "array", "items": { "type": "string" } }
|
|
53
78
|
}
|
|
54
79
|
},
|
|
80
|
+
"ownershipSummary": { "$ref": "#/properties/ownershipSummary" },
|
|
55
81
|
"duplicates": {
|
|
56
82
|
"type": "array",
|
|
57
83
|
"items": {
|
|
58
84
|
"type": "object",
|
|
59
|
-
"required": ["name", "versions", "instances", "confidence", "reasonCodes", "explanation", "recommendation"],
|
|
85
|
+
"required": ["name", "versions", "instances", "workspaceUsage", "rootCause", "confidence", "reasonCodes", "explanation", "recommendation"],
|
|
60
86
|
"additionalProperties": false,
|
|
61
87
|
"properties": {
|
|
62
88
|
"name": { "type": "string" },
|
|
63
89
|
"versions": { "type": "array", "items": { "type": "string" } },
|
|
90
|
+
"workspaceUsage": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"required": ["workspace", "section", "declaredVersion"],
|
|
95
|
+
"additionalProperties": false,
|
|
96
|
+
"properties": {
|
|
97
|
+
"workspace": { "type": "string" },
|
|
98
|
+
"section": { "type": "string", "enum": ["dependencies", "devDependencies"] },
|
|
99
|
+
"declaredVersion": { "type": "string" }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"rootCause": { "type": "array", "items": { "type": "string" } },
|
|
64
104
|
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
65
105
|
"reasonCodes": { "type": "array", "items": { "type": "string" } },
|
|
66
106
|
"explanation": { "type": "array", "items": { "type": "string" } },
|
|
@@ -120,7 +160,7 @@
|
|
|
120
160
|
"type": "array",
|
|
121
161
|
"items": {
|
|
122
162
|
"type": "object",
|
|
123
|
-
"required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "recommendation"],
|
|
163
|
+
"required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "trustScore", "riskFactors", "recommendation"],
|
|
124
164
|
"additionalProperties": false,
|
|
125
165
|
"properties": {
|
|
126
166
|
"name": { "type": "string" },
|
|
@@ -129,6 +169,8 @@
|
|
|
129
169
|
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
130
170
|
"reasonCodes": { "type": "array", "items": { "type": "string" } },
|
|
131
171
|
"explanation": { "type": "array", "items": { "type": "string" } },
|
|
172
|
+
"trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
|
|
173
|
+
"riskFactors": { "$ref": "#/properties/riskFactors" },
|
|
132
174
|
"recommendation": { "$ref": "#/properties/recommendation" }
|
|
133
175
|
}
|
|
134
176
|
}
|
|
@@ -147,6 +189,7 @@
|
|
|
147
189
|
"priority": { "type": "string", "enum": ["high", "medium", "low"] },
|
|
148
190
|
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
149
191
|
"summary": { "type": "string" },
|
|
192
|
+
"trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
|
|
150
193
|
"recommendation": { "$ref": "#/properties/recommendation" }
|
|
151
194
|
}
|
|
152
195
|
}
|
package/dist/checks/duplicate.js
CHANGED
|
@@ -4,6 +4,8 @@ export async function findDuplicateDependencies(graph) {
|
|
|
4
4
|
name,
|
|
5
5
|
versions: Array.from(new Set(instances.map((instance) => instance.version))).sort(),
|
|
6
6
|
instances,
|
|
7
|
+
workspaceUsage: [],
|
|
8
|
+
rootCause: [],
|
|
7
9
|
confidence: 0.98,
|
|
8
10
|
reasonCodes: [
|
|
9
11
|
"multiple_lockfile_versions",
|
package/dist/checks/risk.d.ts
CHANGED
|
@@ -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
|
-
|
|
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>;
|
package/dist/checks/risk.js
CHANGED
|
@@ -1,50 +1,41 @@
|
|
|
1
1
|
import { getPackageMetadata } from "../utils/npm-api.js";
|
|
2
2
|
const TWO_YEARS_IN_DAYS = 365 * 2;
|
|
3
|
-
|
|
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
|
|
10
|
-
const reasons = [];
|
|
11
|
+
const metadata = await resolvePackageMetadata(name);
|
|
11
12
|
if (!metadata) {
|
|
12
13
|
return null;
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (
|
|
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:
|
|
30
|
-
reasonCodes:
|
|
31
|
-
explanation: reasons,
|
|
32
|
-
|
|
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)
|
|
33
33
|
};
|
|
34
34
|
}));
|
|
35
35
|
return results
|
|
36
36
|
.filter((item) => item !== null)
|
|
37
37
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
38
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
|
-
}
|
|
48
39
|
export async function runRiskCheck(graph) {
|
|
49
40
|
const risks = await findRiskDependencies(graph);
|
|
50
41
|
return {
|
|
@@ -53,24 +44,89 @@ export async function runRiskCheck(graph) {
|
|
|
53
44
|
issues: risks.map((item) => ({
|
|
54
45
|
id: `risk:${item.name}`,
|
|
55
46
|
message: `${item.name}: ${item.reasons.join("; ")}`,
|
|
56
|
-
severity: "
|
|
47
|
+
severity: item.trustScore === "low"
|
|
48
|
+
? "critical"
|
|
49
|
+
: item.trustScore === "medium"
|
|
50
|
+
? "warning"
|
|
51
|
+
: "info",
|
|
57
52
|
confidence: item.confidence,
|
|
58
53
|
reasonCodes: item.reasonCodes,
|
|
59
54
|
explanation: item.explanation,
|
|
60
55
|
meta: {
|
|
61
56
|
name: item.name,
|
|
62
|
-
reasons: item.reasons
|
|
57
|
+
reasons: item.reasons,
|
|
58
|
+
trustScore: item.trustScore,
|
|
59
|
+
riskFactors: item.riskFactors
|
|
63
60
|
}
|
|
64
61
|
}))
|
|
65
62
|
};
|
|
66
63
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
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
|
+
};
|
|
69
121
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
};
|
|
76
132
|
}
|
package/dist/core/analyzer.d.ts
CHANGED
|
@@ -8,6 +8,17 @@ export interface DuplicateInstance {
|
|
|
8
8
|
path: string;
|
|
9
9
|
version: string;
|
|
10
10
|
}
|
|
11
|
+
export interface WorkspaceDependencyUsage {
|
|
12
|
+
workspace: string;
|
|
13
|
+
section: "dependencies" | "devDependencies";
|
|
14
|
+
declaredVersion: string;
|
|
15
|
+
}
|
|
16
|
+
export interface WorkspaceOwnershipSummary {
|
|
17
|
+
duplicates: number;
|
|
18
|
+
unused: number;
|
|
19
|
+
outdated: number;
|
|
20
|
+
risks: number;
|
|
21
|
+
}
|
|
11
22
|
export interface Recommendation {
|
|
12
23
|
action: "remove" | "consolidate" | "upgrade" | "review";
|
|
13
24
|
priority: "high" | "medium" | "low";
|
|
@@ -15,10 +26,22 @@ export interface Recommendation {
|
|
|
15
26
|
summary: string;
|
|
16
27
|
reasons: string[];
|
|
17
28
|
}
|
|
29
|
+
export interface RiskFactors {
|
|
30
|
+
daysSincePublish: number | null;
|
|
31
|
+
downloads: number | null;
|
|
32
|
+
maintainersCount: number | null;
|
|
33
|
+
versionCount: number | null;
|
|
34
|
+
recentReleaseCount: number | null;
|
|
35
|
+
hasRepository: boolean;
|
|
36
|
+
dependencyType: "dependencies" | "devDependencies" | "unknown";
|
|
37
|
+
}
|
|
38
|
+
export type TrustScore = "high" | "medium" | "low";
|
|
18
39
|
export interface DuplicateDependency {
|
|
19
40
|
name: string;
|
|
20
41
|
versions: string[];
|
|
21
42
|
instances: DuplicateInstance[];
|
|
43
|
+
workspaceUsage: WorkspaceDependencyUsage[];
|
|
44
|
+
rootCause: string[];
|
|
22
45
|
confidence: number;
|
|
23
46
|
reasonCodes: string[];
|
|
24
47
|
explanation: string[];
|
|
@@ -51,6 +74,8 @@ export interface RiskDependency {
|
|
|
51
74
|
confidence: number;
|
|
52
75
|
reasonCodes: string[];
|
|
53
76
|
explanation: string[];
|
|
77
|
+
trustScore: TrustScore;
|
|
78
|
+
riskFactors: RiskFactors;
|
|
54
79
|
recommendation: Recommendation;
|
|
55
80
|
}
|
|
56
81
|
export interface TopIssue {
|
|
@@ -60,6 +85,7 @@ export interface TopIssue {
|
|
|
60
85
|
priority: "high" | "medium" | "low";
|
|
61
86
|
confidence: number;
|
|
62
87
|
summary: string;
|
|
88
|
+
trustScore?: TrustScore;
|
|
63
89
|
recommendation: Recommendation;
|
|
64
90
|
}
|
|
65
91
|
export interface AnalysisResult {
|
|
@@ -68,6 +94,7 @@ export interface AnalysisResult {
|
|
|
68
94
|
score: number;
|
|
69
95
|
scoreBreakdown: ScoreBreakdown;
|
|
70
96
|
policy: PolicyResult;
|
|
97
|
+
ownershipSummary: WorkspaceOwnershipSummary;
|
|
71
98
|
duplicates: DuplicateDependency[];
|
|
72
99
|
unused: UnusedDependency[];
|
|
73
100
|
outdated: OutdatedDependency[];
|
|
@@ -87,6 +114,7 @@ export interface PackageAnalysisResult {
|
|
|
87
114
|
score: number;
|
|
88
115
|
scoreBreakdown: ScoreBreakdown;
|
|
89
116
|
policy: PolicyResult;
|
|
117
|
+
ownershipSummary: WorkspaceOwnershipSummary;
|
|
90
118
|
duplicates: DuplicateDependency[];
|
|
91
119
|
unused: UnusedDependency[];
|
|
92
120
|
outdated: OutdatedDependency[];
|
|
@@ -94,7 +122,7 @@ export interface PackageAnalysisResult {
|
|
|
94
122
|
suggestions: string[];
|
|
95
123
|
topIssues: TopIssue[];
|
|
96
124
|
}
|
|
97
|
-
export declare const OUTPUT_VERSION = "1.
|
|
125
|
+
export declare const OUTPUT_VERSION = "1.4";
|
|
98
126
|
export interface ScoreBreakdown {
|
|
99
127
|
baseScore: number;
|
|
100
128
|
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.4";
|
|
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);
|
|
@@ -28,6 +28,11 @@ export async function analyzeProject(options = {}) {
|
|
|
28
28
|
});
|
|
29
29
|
packages.push({ ...result, name: workspace.name });
|
|
30
30
|
}
|
|
31
|
+
const workspaceGraphs = await Promise.all(workspaces.map(async (workspace) => ({
|
|
32
|
+
name: workspace.name,
|
|
33
|
+
graph: await buildDependencyGraph(workspace.rootDir)
|
|
34
|
+
})));
|
|
35
|
+
const attributedDuplicates = addWorkspaceAttribution(duplicates, workspaceGraphs);
|
|
31
36
|
const unused = packages.flatMap((pkg) => pkg.unused.map((item) => ({ ...item, package: pkg.name })));
|
|
32
37
|
const outdated = packages.flatMap((pkg) => pkg.outdated.map((item) => ({ ...item, package: pkg.name })));
|
|
33
38
|
const risks = packages.flatMap((pkg) => pkg.risks.map((item) => ({ ...item, package: pkg.name })));
|
|
@@ -52,7 +57,7 @@ export async function analyzeProject(options = {}) {
|
|
|
52
57
|
].slice(0, config.report.maxSuggestions);
|
|
53
58
|
const policy = evaluatePolicy({
|
|
54
59
|
score,
|
|
55
|
-
duplicates:
|
|
60
|
+
duplicates: attributedDuplicates.length,
|
|
56
61
|
unused: unused.length,
|
|
57
62
|
outdated: outdated.length,
|
|
58
63
|
risks: risks.length
|
|
@@ -63,12 +68,23 @@ export async function analyzeProject(options = {}) {
|
|
|
63
68
|
score,
|
|
64
69
|
scoreBreakdown,
|
|
65
70
|
policy,
|
|
66
|
-
|
|
71
|
+
ownershipSummary: buildOwnershipSummary({
|
|
72
|
+
duplicates: attributedDuplicates,
|
|
73
|
+
unused,
|
|
74
|
+
outdated,
|
|
75
|
+
risks
|
|
76
|
+
}),
|
|
77
|
+
duplicates: attributedDuplicates,
|
|
67
78
|
unused,
|
|
68
79
|
outdated,
|
|
69
80
|
risks,
|
|
70
81
|
suggestions,
|
|
71
|
-
topIssues: buildTopIssues({
|
|
82
|
+
topIssues: buildTopIssues({
|
|
83
|
+
duplicates: attributedDuplicates,
|
|
84
|
+
unused,
|
|
85
|
+
outdated,
|
|
86
|
+
risks
|
|
87
|
+
}),
|
|
72
88
|
config,
|
|
73
89
|
packages
|
|
74
90
|
};
|
|
@@ -182,6 +198,12 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
|
182
198
|
score,
|
|
183
199
|
scoreBreakdown,
|
|
184
200
|
policy,
|
|
201
|
+
ownershipSummary: buildOwnershipSummary({
|
|
202
|
+
duplicates,
|
|
203
|
+
unused: scopedUnused,
|
|
204
|
+
outdated: scopedOutdated,
|
|
205
|
+
risks: scopedRisks
|
|
206
|
+
}),
|
|
185
207
|
duplicates,
|
|
186
208
|
unused: scopedUnused,
|
|
187
209
|
outdated: scopedOutdated,
|
|
@@ -272,6 +294,8 @@ function mapDuplicateIssues(issues) {
|
|
|
272
294
|
instances: Array.isArray(issue.meta?.instances)
|
|
273
295
|
? issue.meta?.instances
|
|
274
296
|
: [],
|
|
297
|
+
workspaceUsage: normalizeWorkspaceUsage(issue.meta?.workspaceUsage),
|
|
298
|
+
rootCause: normalizeStringArray(issue.meta?.rootCause),
|
|
275
299
|
confidence: normalizeConfidence(issue.confidence),
|
|
276
300
|
reasonCodes: normalizeStringArray(issue.reasonCodes),
|
|
277
301
|
explanation: normalizeStringArray(issue.explanation),
|
|
@@ -309,6 +333,8 @@ function mapRiskIssues(issues) {
|
|
|
309
333
|
confidence: normalizeConfidence(issue.confidence),
|
|
310
334
|
reasonCodes: normalizeStringArray(issue.reasonCodes),
|
|
311
335
|
explanation: normalizeStringArray(issue.explanation),
|
|
336
|
+
trustScore: normalizeTrustScore(issue.meta?.trustScore),
|
|
337
|
+
riskFactors: normalizeRiskFactors(issue.meta?.riskFactors),
|
|
312
338
|
recommendation: buildRiskRecommendation(issue)
|
|
313
339
|
}));
|
|
314
340
|
}
|
|
@@ -371,11 +397,14 @@ function buildOutdatedRecommendation(issue) {
|
|
|
371
397
|
function buildRiskRecommendation(issue) {
|
|
372
398
|
const reasons = normalizeStringArray(issue.explanation);
|
|
373
399
|
const confidence = normalizeConfidence(issue.confidence);
|
|
400
|
+
const trustScore = normalizeTrustScore(issue.meta?.trustScore);
|
|
374
401
|
return {
|
|
375
402
|
action: "review",
|
|
376
|
-
priority: confidence >= 0.79 ? "high" : "medium",
|
|
403
|
+
priority: trustScore === "low" || confidence >= 0.79 ? "high" : "medium",
|
|
377
404
|
safety: "caution",
|
|
378
|
-
summary:
|
|
405
|
+
summary: trustScore === "low"
|
|
406
|
+
? "Low trust package; review whether to replace, pin, or monitor it closely."
|
|
407
|
+
: "Review package trust signals and decide whether to keep, replace, or monitor it.",
|
|
379
408
|
reasons
|
|
380
409
|
};
|
|
381
410
|
}
|
|
@@ -414,17 +443,60 @@ function buildTopIssues(input) {
|
|
|
414
443
|
priority: item.recommendation.priority,
|
|
415
444
|
confidence: item.confidence,
|
|
416
445
|
summary: item.recommendation.summary,
|
|
446
|
+
trustScore: item.trustScore,
|
|
417
447
|
recommendation: item.recommendation
|
|
418
448
|
}))
|
|
419
449
|
];
|
|
420
450
|
return issues
|
|
421
|
-
.sort((left, right) => comparePriority(right.priority, left.priority) ||
|
|
451
|
+
.sort((left, right) => comparePriority(right.priority, left.priority) ||
|
|
452
|
+
compareTrustScore(right.trustScore, left.trustScore) ||
|
|
453
|
+
right.confidence - left.confidence)
|
|
422
454
|
.slice(0, 5);
|
|
423
455
|
}
|
|
424
456
|
function comparePriority(left, right) {
|
|
425
457
|
const rank = { high: 3, medium: 2, low: 1 };
|
|
426
458
|
return rank[left] - rank[right];
|
|
427
459
|
}
|
|
460
|
+
function compareTrustScore(left, right) {
|
|
461
|
+
const rank = { low: 3, medium: 2, high: 1, undefined: 0 };
|
|
462
|
+
return rank[left ?? "undefined"] - rank[right ?? "undefined"];
|
|
463
|
+
}
|
|
464
|
+
function addWorkspaceAttribution(duplicates, workspaceGraphs) {
|
|
465
|
+
return duplicates.map((item) => {
|
|
466
|
+
const usage = [];
|
|
467
|
+
for (const workspace of workspaceGraphs) {
|
|
468
|
+
const runtimeVersion = workspace.graph.dependencies[item.name];
|
|
469
|
+
if (runtimeVersion) {
|
|
470
|
+
usage.push({
|
|
471
|
+
workspace: workspace.name,
|
|
472
|
+
section: "dependencies",
|
|
473
|
+
declaredVersion: runtimeVersion
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const devVersion = workspace.graph.devDependencies[item.name];
|
|
477
|
+
if (devVersion) {
|
|
478
|
+
usage.push({
|
|
479
|
+
workspace: workspace.name,
|
|
480
|
+
section: "devDependencies",
|
|
481
|
+
declaredVersion: devVersion
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
...item,
|
|
487
|
+
workspaceUsage: usage,
|
|
488
|
+
rootCause: usage.map((entry) => `${entry.workspace} -> ${item.name}@${entry.declaredVersion}`)
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function buildOwnershipSummary(input) {
|
|
493
|
+
return {
|
|
494
|
+
duplicates: input.duplicates.length,
|
|
495
|
+
unused: input.unused.length,
|
|
496
|
+
outdated: input.outdated.length,
|
|
497
|
+
risks: input.risks.length
|
|
498
|
+
};
|
|
499
|
+
}
|
|
428
500
|
function normalizeConfidence(value) {
|
|
429
501
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
430
502
|
return 0.5;
|
|
@@ -437,6 +509,59 @@ function normalizeStringArray(value) {
|
|
|
437
509
|
}
|
|
438
510
|
return value.filter((entry) => typeof entry === "string");
|
|
439
511
|
}
|
|
512
|
+
function normalizeTrustScore(value) {
|
|
513
|
+
return value === "high" || value === "medium" || value === "low"
|
|
514
|
+
? value
|
|
515
|
+
: "medium";
|
|
516
|
+
}
|
|
517
|
+
function normalizeRiskFactors(value) {
|
|
518
|
+
if (!value || typeof value !== "object") {
|
|
519
|
+
return {
|
|
520
|
+
daysSincePublish: null,
|
|
521
|
+
downloads: null,
|
|
522
|
+
maintainersCount: null,
|
|
523
|
+
versionCount: null,
|
|
524
|
+
recentReleaseCount: null,
|
|
525
|
+
hasRepository: false,
|
|
526
|
+
dependencyType: "unknown"
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const factors = value;
|
|
530
|
+
return {
|
|
531
|
+
daysSincePublish: typeof factors.daysSincePublish === "number" ? factors.daysSincePublish : null,
|
|
532
|
+
downloads: typeof factors.downloads === "number" ? factors.downloads : null,
|
|
533
|
+
maintainersCount: typeof factors.maintainersCount === "number" ? factors.maintainersCount : null,
|
|
534
|
+
versionCount: typeof factors.versionCount === "number" ? factors.versionCount : null,
|
|
535
|
+
recentReleaseCount: typeof factors.recentReleaseCount === "number" ? factors.recentReleaseCount : null,
|
|
536
|
+
hasRepository: factors.hasRepository === true,
|
|
537
|
+
dependencyType: factors.dependencyType === "dependencies" || factors.dependencyType === "devDependencies"
|
|
538
|
+
? factors.dependencyType
|
|
539
|
+
: "unknown"
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function normalizeWorkspaceUsage(value) {
|
|
543
|
+
if (!Array.isArray(value)) {
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
return value
|
|
547
|
+
.map((entry) => {
|
|
548
|
+
if (!entry || typeof entry !== "object") {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const usage = entry;
|
|
552
|
+
if (typeof usage.workspace !== "string" ||
|
|
553
|
+
typeof usage.declaredVersion !== "string" ||
|
|
554
|
+
(usage.section !== "dependencies" && usage.section !== "devDependencies")) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
workspace: usage.workspace,
|
|
559
|
+
section: usage.section,
|
|
560
|
+
declaredVersion: usage.declaredVersion
|
|
561
|
+
};
|
|
562
|
+
})
|
|
563
|
+
.filter((entry) => entry !== null);
|
|
564
|
+
}
|
|
440
565
|
function buildScoreBreakdown(counts, config) {
|
|
441
566
|
return {
|
|
442
567
|
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, Recommendation, ScoreBreakdown, RiskDependency, TopIssue, UnusedDependency } from "./core/analyzer.js";
|
|
2
|
+
export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } 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";
|
|
@@ -3,7 +3,7 @@ export function renderConsoleReport(result) {
|
|
|
3
3
|
if (result.topIssues.length > 0) {
|
|
4
4
|
lines.push("Top Issues:");
|
|
5
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}`);
|
|
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
7
|
}
|
|
8
8
|
lines.push("");
|
|
9
9
|
}
|
|
@@ -20,10 +20,10 @@ export function renderConsoleReport(result) {
|
|
|
20
20
|
lines.push("");
|
|
21
21
|
lines.push("Packages:");
|
|
22
22
|
for (const pkg of result.packages) {
|
|
23
|
-
lines.push(`- ${pkg.name}: ${pkg.score}/100, D:${pkg.duplicates
|
|
23
|
+
lines.push(`- ${pkg.name}: ${pkg.score}/100, D:${pkg.ownershipSummary.duplicates} U:${pkg.ownershipSummary.unused} O:${pkg.ownershipSummary.outdated} R:${pkg.ownershipSummary.risks}`);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation, item.recommendation)));
|
|
26
|
+
appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}${item.rootCause.length > 0 ? ` | via ${item.rootCause.join("; ")}` : ""}`, item.confidence, item.explanation, item.recommendation)));
|
|
27
27
|
appendSection(lines, "Unused dependencies", result.unused.map((item) => formatEntry(item.package
|
|
28
28
|
? `${item.name} (${item.section}) [${item.package}]`
|
|
29
29
|
: `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
|
|
@@ -31,8 +31,8 @@ export function renderConsoleReport(result) {
|
|
|
31
31
|
? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
|
|
32
32
|
: `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
|
|
33
33
|
appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
|
|
34
|
-
? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
|
|
35
|
-
: `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation, item.recommendation)));
|
|
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)));
|
|
36
36
|
appendSection(lines, "Policy reasons", result.policy.reasons);
|
|
37
37
|
if (result.suggestions.length > 0) {
|
|
38
38
|
lines.push("");
|
|
@@ -5,7 +5,7 @@ export function renderMarkdownReport(result) {
|
|
|
5
5
|
if (result.topIssues.length > 0) {
|
|
6
6
|
lines.push("## Top Issues");
|
|
7
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}`);
|
|
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
9
|
}
|
|
10
10
|
lines.push("");
|
|
11
11
|
}
|
|
@@ -17,7 +17,7 @@ export function renderMarkdownReport(result) {
|
|
|
17
17
|
if (result.packages && result.packages.length > 0) {
|
|
18
18
|
lines.push("## Packages");
|
|
19
19
|
for (const pkg of result.packages) {
|
|
20
|
-
lines.push(`- ${pkg.name}: ${pkg.score}/100 (D:${pkg.duplicates
|
|
20
|
+
lines.push(`- ${pkg.name}: ${pkg.score}/100 (D:${pkg.ownershipSummary.duplicates} U:${pkg.ownershipSummary.unused} O:${pkg.ownershipSummary.outdated} R:${pkg.ownershipSummary.risks})`);
|
|
21
21
|
}
|
|
22
22
|
lines.push("");
|
|
23
23
|
}
|
|
@@ -27,7 +27,7 @@ export function renderMarkdownReport(result) {
|
|
|
27
27
|
lines.push(`- Outdated: ${result.outdated.length}`);
|
|
28
28
|
lines.push(`- Risks: ${result.risks.length}`);
|
|
29
29
|
lines.push("");
|
|
30
|
-
appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation, item.recommendation)));
|
|
30
|
+
appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}${item.rootCause.length > 0 ? ` | via ${item.rootCause.join("; ")}` : ""}`, item.confidence, item.explanation, item.recommendation)));
|
|
31
31
|
appendSection(lines, "Unused dependencies", result.unused.map((item) => formatEntry(item.package
|
|
32
32
|
? `${item.name} (${item.section}) [${item.package}]`
|
|
33
33
|
: `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
|
|
@@ -35,8 +35,8 @@ export function renderMarkdownReport(result) {
|
|
|
35
35
|
? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
|
|
36
36
|
: `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation, item.recommendation)));
|
|
37
37
|
appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
|
|
38
|
-
? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
|
|
39
|
-
: `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation, item.recommendation)));
|
|
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)));
|
|
40
40
|
appendSection(lines, "Policy reasons", result.policy.reasons);
|
|
41
41
|
if (result.suggestions.length > 0) {
|
|
42
42
|
lines.push("## Suggestions");
|
package/dist/utils/npm-api.d.ts
CHANGED
|
@@ -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>;
|
package/dist/utils/npm-api.js
CHANGED
|
@@ -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
|
+
}
|