dep-brain 0.8.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 +3 -1
- package/README.md +6 -0
- package/depbrain.output.schema.json +28 -2
- package/dist/checks/duplicate.js +2 -0
- package/dist/core/analyzer.d.ts +16 -1
- package/dist/core/analyzer.js +87 -4
- package/dist/index.d.ts +1 -1
- package/dist/reporters/console.js +2 -2
- package/dist/reporters/markdown.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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.
|
|
@@ -11,3 +11,5 @@ All notable changes to this project will be documented in this file.
|
|
|
11
11
|
- Ranked top-issues summary output with `--top`.
|
|
12
12
|
- Supply-chain trust scoring for risk findings.
|
|
13
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
|
@@ -75,6 +75,12 @@ dep-brain --version
|
|
|
75
75
|
|
|
76
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.
|
|
77
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
|
+
|
|
78
84
|
## Example Output
|
|
79
85
|
|
|
80
86
|
```text
|
|
@@ -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": {
|
|
@@ -31,6 +31,17 @@
|
|
|
31
31
|
"dependencyType": { "type": "string", "enum": ["dependencies", "devDependencies", "unknown"] }
|
|
32
32
|
}
|
|
33
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
|
+
},
|
|
34
45
|
"outputVersion": { "type": "string" },
|
|
35
46
|
"rootDir": { "type": "string" },
|
|
36
47
|
"score": { "type": "number" },
|
|
@@ -66,15 +77,30 @@
|
|
|
66
77
|
"reasons": { "type": "array", "items": { "type": "string" } }
|
|
67
78
|
}
|
|
68
79
|
},
|
|
80
|
+
"ownershipSummary": { "$ref": "#/properties/ownershipSummary" },
|
|
69
81
|
"duplicates": {
|
|
70
82
|
"type": "array",
|
|
71
83
|
"items": {
|
|
72
84
|
"type": "object",
|
|
73
|
-
"required": ["name", "versions", "instances", "confidence", "reasonCodes", "explanation", "recommendation"],
|
|
85
|
+
"required": ["name", "versions", "instances", "workspaceUsage", "rootCause", "confidence", "reasonCodes", "explanation", "recommendation"],
|
|
74
86
|
"additionalProperties": false,
|
|
75
87
|
"properties": {
|
|
76
88
|
"name": { "type": "string" },
|
|
77
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" } },
|
|
78
104
|
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
79
105
|
"reasonCodes": { "type": "array", "items": { "type": "string" } },
|
|
80
106
|
"explanation": { "type": "array", "items": { "type": "string" } },
|
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/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";
|
|
@@ -29,6 +40,8 @@ export interface DuplicateDependency {
|
|
|
29
40
|
name: string;
|
|
30
41
|
versions: string[];
|
|
31
42
|
instances: DuplicateInstance[];
|
|
43
|
+
workspaceUsage: WorkspaceDependencyUsage[];
|
|
44
|
+
rootCause: string[];
|
|
32
45
|
confidence: number;
|
|
33
46
|
reasonCodes: string[];
|
|
34
47
|
explanation: string[];
|
|
@@ -81,6 +94,7 @@ export interface AnalysisResult {
|
|
|
81
94
|
score: number;
|
|
82
95
|
scoreBreakdown: ScoreBreakdown;
|
|
83
96
|
policy: PolicyResult;
|
|
97
|
+
ownershipSummary: WorkspaceOwnershipSummary;
|
|
84
98
|
duplicates: DuplicateDependency[];
|
|
85
99
|
unused: UnusedDependency[];
|
|
86
100
|
outdated: OutdatedDependency[];
|
|
@@ -100,6 +114,7 @@ export interface PackageAnalysisResult {
|
|
|
100
114
|
score: number;
|
|
101
115
|
scoreBreakdown: ScoreBreakdown;
|
|
102
116
|
policy: PolicyResult;
|
|
117
|
+
ownershipSummary: WorkspaceOwnershipSummary;
|
|
103
118
|
duplicates: DuplicateDependency[];
|
|
104
119
|
unused: UnusedDependency[];
|
|
105
120
|
outdated: OutdatedDependency[];
|
|
@@ -107,7 +122,7 @@ export interface PackageAnalysisResult {
|
|
|
107
122
|
suggestions: string[];
|
|
108
123
|
topIssues: TopIssue[];
|
|
109
124
|
}
|
|
110
|
-
export declare const OUTPUT_VERSION = "1.
|
|
125
|
+
export declare const OUTPUT_VERSION = "1.4";
|
|
111
126
|
export interface ScoreBreakdown {
|
|
112
127
|
baseScore: number;
|
|
113
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),
|
|
@@ -437,6 +461,42 @@ function compareTrustScore(left, right) {
|
|
|
437
461
|
const rank = { low: 3, medium: 2, high: 1, undefined: 0 };
|
|
438
462
|
return rank[left ?? "undefined"] - rank[right ?? "undefined"];
|
|
439
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
|
+
}
|
|
440
500
|
function normalizeConfidence(value) {
|
|
441
501
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
442
502
|
return 0.5;
|
|
@@ -479,6 +539,29 @@ function normalizeRiskFactors(value) {
|
|
|
479
539
|
: "unknown"
|
|
480
540
|
};
|
|
481
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
|
+
}
|
|
482
565
|
function buildScoreBreakdown(counts, config) {
|
|
483
566
|
return {
|
|
484
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";
|
|
@@ -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)));
|
|
@@ -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)));
|