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 CHANGED
@@ -2,10 +2,14 @@
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.
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
  }
@@ -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",
@@ -1,5 +1,9 @@
1
1
  import type { DependencyGraph } from "../core/graph-builder.js";
2
2
  import type { RiskDependency } from "../core/analyzer.js";
3
3
  import type { CheckResult } from "../core/types.js";
4
- export declare function findRiskDependencies(graph: DependencyGraph): Promise<RiskDependency[]>;
4
+ import { type PackageMetadata } from "../utils/npm-api.js";
5
+ export interface RiskCheckOptions {
6
+ resolvePackageMetadata?: (name: string) => Promise<PackageMetadata | null>;
7
+ }
8
+ export declare function findRiskDependencies(graph: DependencyGraph, options?: RiskCheckOptions): Promise<RiskDependency[]>;
5
9
  export declare function runRiskCheck(graph: DependencyGraph): Promise<CheckResult>;
@@ -1,50 +1,41 @@
1
1
  import { getPackageMetadata } from "../utils/npm-api.js";
2
2
  const TWO_YEARS_IN_DAYS = 365 * 2;
3
- export async function findRiskDependencies(graph) {
3
+ const ONE_YEAR_IN_DAYS = 365;
4
+ export async function findRiskDependencies(graph, options = {}) {
5
+ const resolvePackageMetadata = options.resolvePackageMetadata ?? getPackageMetadata;
4
6
  const names = Object.keys({
5
7
  ...graph.dependencies,
6
8
  ...graph.devDependencies
7
9
  });
8
10
  const results = await Promise.all(names.map(async (name) => {
9
- const metadata = await getPackageMetadata(name);
10
- const reasons = [];
11
+ const metadata = await resolvePackageMetadata(name);
11
12
  if (!metadata) {
12
13
  return null;
13
14
  }
14
- if (metadata.daysSincePublish !== null && metadata.daysSincePublish > TWO_YEARS_IN_DAYS) {
15
- reasons.push("No release in over 2 years");
16
- }
17
- if (metadata.downloads !== null && metadata.downloads < 1000) {
18
- reasons.push("Low weekly download volume");
19
- }
20
- if (!metadata.repository) {
21
- reasons.push("Missing repository metadata");
22
- }
23
- if (reasons.length === 0) {
15
+ const dependencyType = graph.dependencies[name]
16
+ ? "dependencies"
17
+ : graph.devDependencies[name]
18
+ ? "devDependencies"
19
+ : "unknown";
20
+ const assessment = assessRisk(metadata, dependencyType);
21
+ if (assessment.reasons.length === 0) {
24
22
  return null;
25
23
  }
26
24
  return {
27
25
  name,
28
- reasons,
29
- confidence: calculateRiskConfidence(reasons),
30
- reasonCodes: reasons.map(toRiskReasonCode),
31
- explanation: reasons,
32
- recommendation: buildRiskRecommendation(reasons, calculateRiskConfidence(reasons))
26
+ reasons: assessment.reasons,
27
+ confidence: assessment.confidence,
28
+ reasonCodes: assessment.reasonCodes,
29
+ explanation: assessment.reasons,
30
+ trustScore: assessment.trustScore,
31
+ riskFactors: assessment.riskFactors,
32
+ recommendation: buildRiskRecommendation(assessment.reasons, assessment.confidence, assessment.trustScore)
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: "warning",
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 calculateRiskConfidence(reasons) {
68
- return Math.min(0.99, 0.55 + reasons.length * 0.12);
64
+ function assessRisk(metadata, dependencyType) {
65
+ const reasons = [];
66
+ const reasonCodes = [];
67
+ let weight = 0;
68
+ if (metadata.daysSincePublish !== null && metadata.daysSincePublish > TWO_YEARS_IN_DAYS) {
69
+ reasons.push("No release in over 2 years");
70
+ reasonCodes.push("stale_release");
71
+ weight += 3;
72
+ }
73
+ else if (metadata.daysSincePublish !== null &&
74
+ metadata.daysSincePublish > ONE_YEAR_IN_DAYS) {
75
+ reasons.push("No release in over 12 months");
76
+ reasonCodes.push("aging_release");
77
+ weight += 2;
78
+ }
79
+ if (metadata.downloads !== null && metadata.downloads < 1000) {
80
+ reasons.push("Low weekly download volume");
81
+ reasonCodes.push("low_download_volume");
82
+ weight += 2;
83
+ }
84
+ if (!metadata.repository) {
85
+ reasons.push("Missing repository metadata");
86
+ reasonCodes.push("missing_repository_metadata");
87
+ weight += 2;
88
+ }
89
+ if (metadata.maintainersCount !== null && metadata.maintainersCount <= 1) {
90
+ reasons.push("Single maintainer package");
91
+ reasonCodes.push("single_maintainer");
92
+ weight += 2;
93
+ }
94
+ if (metadata.recentReleaseCount !== null && metadata.recentReleaseCount === 0) {
95
+ reasons.push("No releases published in the last 30 days");
96
+ reasonCodes.push("no_recent_release");
97
+ weight += 1;
98
+ }
99
+ if (metadata.versionCount !== null && metadata.versionCount <= 3) {
100
+ reasons.push("Very limited published version history");
101
+ reasonCodes.push("limited_version_history");
102
+ weight += 1;
103
+ }
104
+ const confidence = reasons.length === 0 ? 0.5 : Math.min(0.99, 0.52 + weight * 0.07);
105
+ const trustScore = weight >= 6 ? "low" : weight >= 3 ? "medium" : "high";
106
+ return {
107
+ confidence,
108
+ trustScore,
109
+ reasons,
110
+ reasonCodes,
111
+ riskFactors: {
112
+ daysSincePublish: metadata.daysSincePublish,
113
+ downloads: metadata.downloads,
114
+ maintainersCount: metadata.maintainersCount,
115
+ versionCount: metadata.versionCount,
116
+ recentReleaseCount: metadata.recentReleaseCount,
117
+ hasRepository: Boolean(metadata.repository),
118
+ dependencyType
119
+ }
120
+ };
69
121
  }
70
- function toRiskReasonCode(reason) {
71
- const normalized = reason
72
- .toLowerCase()
73
- .replace(/[^a-z0-9]+/g, "_")
74
- .replace(/^_+|_+$/g, "");
75
- return normalized || "risk_signal_detected";
122
+ function buildRiskRecommendation(reasons, confidence, trustScore) {
123
+ return {
124
+ action: "review",
125
+ priority: trustScore === "low" || confidence >= 0.8 ? "high" : "medium",
126
+ safety: "caution",
127
+ summary: trustScore === "low"
128
+ ? "Low trust package; review whether to replace, pin, or monitor it closely."
129
+ : "Review package trust signals and decide whether to keep, replace, or monitor it.",
130
+ reasons
131
+ };
76
132
  }
@@ -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.2";
125
+ export declare const OUTPUT_VERSION = "1.4";
98
126
  export interface ScoreBreakdown {
99
127
  baseScore: number;
100
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.2";
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),
@@ -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: "Review package trust signals and decide whether to keep, replace, or monitor it.",
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) || right.confidence - left.confidence)
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.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)));
@@ -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.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)));
@@ -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");
@@ -3,6 +3,9 @@ export interface PackageMetadata {
3
3
  repository: string | null;
4
4
  downloads: number | null;
5
5
  daysSincePublish: number | null;
6
+ maintainersCount: number | null;
7
+ versionCount: number | null;
8
+ recentReleaseCount: number | null;
6
9
  }
7
10
  export declare function getLatestVersion(name: string): Promise<string | null>;
8
11
  export declare function getPackageMetadata(name: string): Promise<PackageMetadata | null>;
@@ -39,14 +39,35 @@ async function fetchPackageMetadata(name) {
39
39
  const repository = typeof packageJson.repository === "string"
40
40
  ? packageJson.repository
41
41
  : packageJson.repository?.url ?? null;
42
+ const maintainersCount = Array.isArray(packageJson.maintainers)
43
+ ? packageJson.maintainers.length
44
+ : null;
45
+ const versionCount = packageJson.versions
46
+ ? Object.keys(packageJson.versions).length
47
+ : null;
48
+ const recentReleaseCount = countRecentReleases(packageJson.time ?? {});
42
49
  return {
43
50
  latestVersion,
44
51
  repository,
45
52
  downloads: downloadsJson.downloads ?? null,
46
- daysSincePublish
53
+ daysSincePublish,
54
+ maintainersCount,
55
+ versionCount,
56
+ recentReleaseCount
47
57
  };
48
58
  }
49
59
  catch {
50
60
  return null;
51
61
  }
52
62
  }
63
+ function countRecentReleases(time) {
64
+ const values = Object.entries(time)
65
+ .filter(([key]) => key !== "created" && key !== "modified")
66
+ .map(([, value]) => new Date(value).getTime())
67
+ .filter((value) => Number.isFinite(value));
68
+ if (values.length === 0) {
69
+ return null;
70
+ }
71
+ const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
72
+ return values.filter((value) => value >= thirtyDaysAgo).length;
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "0.7.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",