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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## Unreleased
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" } },
@@ -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",
@@ -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.3";
125
+ export declare const OUTPUT_VERSION = "1.4";
111
126
  export interface ScoreBreakdown {
112
127
  baseScore: number;
113
128
  duplicates: number;
@@ -8,7 +8,7 @@ import { findWorkspacePackages } from "../utils/workspaces.js";
8
8
  import { buildDependencyGraph } from "./graph-builder.js";
9
9
  import { calculateHealthScore } from "./scorer.js";
10
10
  import { buildAnalysisContext } from "./context.js";
11
- export const OUTPUT_VERSION = "1.3";
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: duplicates.length,
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
- duplicates,
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({ duplicates, unused, outdated, risks }),
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.length} U:${pkg.unused.length} O:${pkg.outdated.length} R:${pkg.risks.length}`);
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.length} U:${pkg.unused.length} O:${pkg.outdated.length} R:${pkg.risks.length})`);
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)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "CLI and library for dependency health analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",