dep-brain 0.7.0 → 0.8.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
@@ -9,3 +9,5 @@ All notable changes to this project will be documented in this file.
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.
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
 
@@ -257,6 +258,8 @@ src/
257
258
 
258
259
  The project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
259
260
 
261
+ Risk findings now include a `trustScore` plus structured `riskFactors` such as publish recency, maintainer count, and repository presence.
262
+
260
263
  ## Repository Notes
261
264
 
262
265
  - Project brief: [docs/project-brief.md](./docs/project-brief.md)
@@ -17,6 +17,20 @@
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
+ },
20
34
  "outputVersion": { "type": "string" },
21
35
  "rootDir": { "type": "string" },
22
36
  "score": { "type": "number" },
@@ -120,7 +134,7 @@
120
134
  "type": "array",
121
135
  "items": {
122
136
  "type": "object",
123
- "required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "recommendation"],
137
+ "required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "trustScore", "riskFactors", "recommendation"],
124
138
  "additionalProperties": false,
125
139
  "properties": {
126
140
  "name": { "type": "string" },
@@ -129,6 +143,8 @@
129
143
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
130
144
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
131
145
  "explanation": { "type": "array", "items": { "type": "string" } },
146
+ "trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
147
+ "riskFactors": { "$ref": "#/properties/riskFactors" },
132
148
  "recommendation": { "$ref": "#/properties/recommendation" }
133
149
  }
134
150
  }
@@ -147,6 +163,7 @@
147
163
  "priority": { "type": "string", "enum": ["high", "medium", "low"] },
148
164
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
149
165
  "summary": { "type": "string" },
166
+ "trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
150
167
  "recommendation": { "$ref": "#/properties/recommendation" }
151
168
  }
152
169
  }
@@ -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
  }
@@ -15,6 +15,16 @@ export interface Recommendation {
15
15
  summary: string;
16
16
  reasons: string[];
17
17
  }
18
+ export interface RiskFactors {
19
+ daysSincePublish: number | null;
20
+ downloads: number | null;
21
+ maintainersCount: number | null;
22
+ versionCount: number | null;
23
+ recentReleaseCount: number | null;
24
+ hasRepository: boolean;
25
+ dependencyType: "dependencies" | "devDependencies" | "unknown";
26
+ }
27
+ export type TrustScore = "high" | "medium" | "low";
18
28
  export interface DuplicateDependency {
19
29
  name: string;
20
30
  versions: string[];
@@ -51,6 +61,8 @@ export interface RiskDependency {
51
61
  confidence: number;
52
62
  reasonCodes: string[];
53
63
  explanation: string[];
64
+ trustScore: TrustScore;
65
+ riskFactors: RiskFactors;
54
66
  recommendation: Recommendation;
55
67
  }
56
68
  export interface TopIssue {
@@ -60,6 +72,7 @@ export interface TopIssue {
60
72
  priority: "high" | "medium" | "low";
61
73
  confidence: number;
62
74
  summary: string;
75
+ trustScore?: TrustScore;
63
76
  recommendation: Recommendation;
64
77
  }
65
78
  export interface AnalysisResult {
@@ -94,7 +107,7 @@ export interface PackageAnalysisResult {
94
107
  suggestions: string[];
95
108
  topIssues: TopIssue[];
96
109
  }
97
- export declare const OUTPUT_VERSION = "1.2";
110
+ export declare const OUTPUT_VERSION = "1.3";
98
111
  export interface ScoreBreakdown {
99
112
  baseScore: number;
100
113
  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.3";
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);
@@ -309,6 +309,8 @@ function mapRiskIssues(issues) {
309
309
  confidence: normalizeConfidence(issue.confidence),
310
310
  reasonCodes: normalizeStringArray(issue.reasonCodes),
311
311
  explanation: normalizeStringArray(issue.explanation),
312
+ trustScore: normalizeTrustScore(issue.meta?.trustScore),
313
+ riskFactors: normalizeRiskFactors(issue.meta?.riskFactors),
312
314
  recommendation: buildRiskRecommendation(issue)
313
315
  }));
314
316
  }
@@ -371,11 +373,14 @@ function buildOutdatedRecommendation(issue) {
371
373
  function buildRiskRecommendation(issue) {
372
374
  const reasons = normalizeStringArray(issue.explanation);
373
375
  const confidence = normalizeConfidence(issue.confidence);
376
+ const trustScore = normalizeTrustScore(issue.meta?.trustScore);
374
377
  return {
375
378
  action: "review",
376
- priority: confidence >= 0.79 ? "high" : "medium",
379
+ priority: trustScore === "low" || confidence >= 0.79 ? "high" : "medium",
377
380
  safety: "caution",
378
- summary: "Review package trust signals and decide whether to keep, replace, or monitor it.",
381
+ summary: trustScore === "low"
382
+ ? "Low trust package; review whether to replace, pin, or monitor it closely."
383
+ : "Review package trust signals and decide whether to keep, replace, or monitor it.",
379
384
  reasons
380
385
  };
381
386
  }
@@ -414,17 +419,24 @@ function buildTopIssues(input) {
414
419
  priority: item.recommendation.priority,
415
420
  confidence: item.confidence,
416
421
  summary: item.recommendation.summary,
422
+ trustScore: item.trustScore,
417
423
  recommendation: item.recommendation
418
424
  }))
419
425
  ];
420
426
  return issues
421
- .sort((left, right) => comparePriority(right.priority, left.priority) || right.confidence - left.confidence)
427
+ .sort((left, right) => comparePriority(right.priority, left.priority) ||
428
+ compareTrustScore(right.trustScore, left.trustScore) ||
429
+ right.confidence - left.confidence)
422
430
  .slice(0, 5);
423
431
  }
424
432
  function comparePriority(left, right) {
425
433
  const rank = { high: 3, medium: 2, low: 1 };
426
434
  return rank[left] - rank[right];
427
435
  }
436
+ function compareTrustScore(left, right) {
437
+ const rank = { low: 3, medium: 2, high: 1, undefined: 0 };
438
+ return rank[left ?? "undefined"] - rank[right ?? "undefined"];
439
+ }
428
440
  function normalizeConfidence(value) {
429
441
  if (typeof value !== "number" || Number.isNaN(value)) {
430
442
  return 0.5;
@@ -437,6 +449,36 @@ function normalizeStringArray(value) {
437
449
  }
438
450
  return value.filter((entry) => typeof entry === "string");
439
451
  }
452
+ function normalizeTrustScore(value) {
453
+ return value === "high" || value === "medium" || value === "low"
454
+ ? value
455
+ : "medium";
456
+ }
457
+ function normalizeRiskFactors(value) {
458
+ if (!value || typeof value !== "object") {
459
+ return {
460
+ daysSincePublish: null,
461
+ downloads: null,
462
+ maintainersCount: null,
463
+ versionCount: null,
464
+ recentReleaseCount: null,
465
+ hasRepository: false,
466
+ dependencyType: "unknown"
467
+ };
468
+ }
469
+ const factors = value;
470
+ return {
471
+ daysSincePublish: typeof factors.daysSincePublish === "number" ? factors.daysSincePublish : null,
472
+ downloads: typeof factors.downloads === "number" ? factors.downloads : null,
473
+ maintainersCount: typeof factors.maintainersCount === "number" ? factors.maintainersCount : null,
474
+ versionCount: typeof factors.versionCount === "number" ? factors.versionCount : null,
475
+ recentReleaseCount: typeof factors.recentReleaseCount === "number" ? factors.recentReleaseCount : null,
476
+ hasRepository: factors.hasRepository === true,
477
+ dependencyType: factors.dependencyType === "dependencies" || factors.dependencyType === "devDependencies"
478
+ ? factors.dependencyType
479
+ : "unknown"
480
+ };
481
+ }
440
482
  function buildScoreBreakdown(counts, config) {
441
483
  return {
442
484
  baseScore: 100,
@@ -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
  }
@@ -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
  }
@@ -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.8.0",
4
4
  "description": "CLI and library for dependency health analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",