dep-brain 1.2.0 → 1.4.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,6 +2,30 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 1.4.0
6
+
7
+ - Added lockfile dependency-edge parsing so transitive relationships are available from npm, pnpm, and yarn lockfiles.
8
+ - Changed risk analysis from direct-package-only output to direct-owner risk summaries with `transitiveRiskScore` and `riskyTransitiveDeps`.
9
+ - Added transitive dependency counts and risky transitive counts to `riskFactors`.
10
+ - Updated console, markdown, and dashboard reports to highlight transitive risk hotspots.
11
+ - Bumped analysis output contract to `1.5` and added regression coverage for transitive risk propagation.
12
+
13
+ ## 1.3.0
14
+
15
+ - Added plugin diagnostics under `extensions.depBrain.plugins` for failed plugin loads and hook errors.
16
+ - Added built-in `license` plugin through `plugins.enabled: ["license"]`.
17
+ - Added configurable risk thresholds for stale release days, aging release days, low downloads, and trust score weights.
18
+ - Added `--dashboard` and `--dashboard-out` for static HTML dashboard generation.
19
+ - Updated starter config, schemas, README, and tests for v1.3 behavior.
20
+
21
+ ## 1.2.0
22
+
23
+ - Added `PluginManager` with `preScan`, `postScan`, and `reportHook` lifecycle support.
24
+ - Added disabled-by-default plugin config through `plugins.enabled` and `plugins.paths`.
25
+ - Added `extensions` to analysis output so plugins can enrich results without breaking schema.
26
+ - Added future config slots for risk thresholds, dashboard output, and notification webhook env names.
27
+ - Added regression coverage for plugin hooks enriching `extensions`.
28
+
5
29
  ## 1.1.0
6
30
 
7
31
  - Added `--focus` modes for targeted duplicate, unused, outdated, risk, and health analysis.
package/README.md CHANGED
@@ -21,9 +21,10 @@
21
21
  - Detect likely unused dependencies from source imports and scripts
22
22
  - Detect outdated packages
23
23
  - Highlight dependency risk signals
24
+ - Show which direct dependency introduces risky transitive packages
24
25
  - Score package trust using supply-chain metadata
25
26
  - Generate a simple project health score
26
- - Output reports in console, JSON, Markdown, SARIF, and top-issues formats
27
+ - Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
27
28
  - Gate CI with score and finding policies
28
29
  - Compare new findings against a baseline report
29
30
 
@@ -39,6 +40,7 @@ The long-term goal is not just to list problems, but to answer:
39
40
  - Unused dependency detection with runtime vs dev-tool heuristics
40
41
  - Outdated dependency reporting with `major`, `minor`, and `patch` classification
41
42
  - Risk analysis based on npm package metadata
43
+ - Transitive risk ownership and path tracing for direct dependencies
42
44
  - Confidence scores, reason codes, explanations, and recommendations for findings
43
45
  - Config loading from `depbrain.config.json`
44
46
  - Ignore rules for noisy dependencies and checks
@@ -48,12 +50,14 @@ The long-term goal is not just to list problems, but to answer:
48
50
  - JSON output via `--json`
49
51
  - Markdown output via `--md`
50
52
  - SARIF output via `--sarif`
53
+ - Static HTML dashboard via `--dashboard`
51
54
  - Ranked top issues via `--top`
52
55
  - Baseline mode via `--baseline`
53
56
  - Focused analysis via `--focus`
54
57
  - Low-noise CI defaults via `--ci`
55
58
  - Starter config generation via `dep-brain init`
56
59
  - Reusable GitHub Action via `action.yml`
60
+ - Built-in `license` plugin and plugin diagnostics
57
61
  - Library entrypoint for programmatic use
58
62
 
59
63
  ## CLI Usage
@@ -73,6 +77,8 @@ npx dep-brain analyze ./path-to-project --fail-on-unused --json
73
77
  npx dep-brain analyze --md > depbrain.md
74
78
  npx dep-brain analyze --json --out depbrain.json
75
79
  npx dep-brain analyze --sarif --out depbrain.sarif
80
+ npx dep-brain analyze --dashboard
81
+ npx dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
76
82
  npx dep-brain analyze --focus duplicates
77
83
  npx dep-brain analyze --ci
78
84
  npx dep-brain analyze --baseline depbrain-baseline.json
@@ -176,6 +182,28 @@ dep-brain analyze --top
176
182
 
177
183
  Shows the highest-priority actionable findings first, including confidence and next-step guidance.
178
184
 
185
+ ## Dashboard Output
186
+
187
+ ```bash
188
+ dep-brain analyze --dashboard
189
+ dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
190
+ ```
191
+
192
+ Writes a static HTML dashboard. Default path comes from `dashboard.outputPath`.
193
+
194
+ ## Plugins
195
+
196
+ ```json
197
+ {
198
+ "plugins": {
199
+ "enabled": ["license"],
200
+ "paths": ["./depbrain-plugin.mjs"]
201
+ }
202
+ }
203
+ ```
204
+
205
+ Built-in `license` plugin adds license counts under `extensions.license`. Failed plugin loads and hook errors are reported under `extensions.depBrain.plugins`.
206
+
179
207
  ## Report From JSON
180
208
 
181
209
  ```bash
@@ -194,30 +222,6 @@ dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-ris
194
222
 
195
223
  The baseline file is a normal JSON analysis report. Matching entries in `duplicates`, `unused`, `outdated`, and `risks` are ignored before score, policy, suggestions, and top issues are calculated.
196
224
 
197
- ## GitHub Action
198
-
199
- ```yaml
200
- name: Dependency Brain
201
-
202
- on:
203
- pull_request:
204
-
205
- jobs:
206
- dep-brain:
207
- runs-on: ubuntu-latest
208
- steps:
209
- - uses: actions/checkout@v4
210
- - uses: actions/setup-node@v4
211
- with:
212
- node-version: 22
213
- - uses: prakashu51/dep-brain@v1
214
- with:
215
- format: sarif
216
- out: depbrain.sarif
217
- min-score: 85
218
- fail-on-risks: "true"
219
- ```
220
-
221
225
  ## Config File
222
226
 
223
227
  Create a `depbrain.config.json` file in the project root:
@@ -239,12 +243,17 @@ Create a `depbrain.config.json` file in the project root:
239
243
  "maxSuggestions": 3
240
244
  },
241
245
  "plugins": {
242
- "enabled": [],
246
+ "enabled": ["license"],
243
247
  "paths": []
244
248
  },
245
249
  "risk": {
246
250
  "transitiveBloatThreshold": 50,
247
- "typosquattingDistanceThreshold": 2
251
+ "typosquattingDistanceThreshold": 2,
252
+ "staleReleaseDays": 730,
253
+ "agingReleaseDays": 365,
254
+ "lowDownloadThreshold": 1000,
255
+ "lowTrustWeightThreshold": 6,
256
+ "mediumTrustWeightThreshold": 3
248
257
  },
249
258
  "dashboard": {
250
259
  "outputPath": "depbrain-dashboard.html"
@@ -283,6 +292,11 @@ Supported sections:
283
292
  - `plugins.paths`
284
293
  - `risk.transitiveBloatThreshold`
285
294
  - `risk.typosquattingDistanceThreshold`
295
+ - `risk.staleReleaseDays`
296
+ - `risk.agingReleaseDays`
297
+ - `risk.lowDownloadThreshold`
298
+ - `risk.lowTrustWeightThreshold`
299
+ - `risk.mediumTrustWeightThreshold`
286
300
  - `dashboard.outputPath`
287
301
  - `notifications.slackWebhookEnv`
288
302
  - `notifications.discordWebhookEnv`
@@ -367,7 +381,7 @@ src/
367
381
 
368
382
  The project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
369
383
 
370
- Risk findings now include a `trustScore` plus structured `riskFactors` such as publish recency, maintainer count, and repository presence.
384
+ Risk findings now include a `trustScore`, structured `riskFactors`, `transitiveRiskScore`, and `riskyTransitiveDeps` path traces so teams can see which direct package introduces supply-chain risk.
371
385
 
372
386
  ## Repository Notes
373
387
 
@@ -25,7 +25,12 @@
25
25
  },
26
26
  "risk": {
27
27
  "transitiveBloatThreshold": 50,
28
- "typosquattingDistanceThreshold": 2
28
+ "typosquattingDistanceThreshold": 2,
29
+ "staleReleaseDays": 730,
30
+ "agingReleaseDays": 365,
31
+ "lowDownloadThreshold": 1000,
32
+ "lowTrustWeightThreshold": 6,
33
+ "mediumTrustWeightThreshold": 3
29
34
  },
30
35
  "dashboard": {
31
36
  "outputPath": "depbrain-dashboard.html"
@@ -49,7 +49,12 @@
49
49
  "additionalProperties": false,
50
50
  "properties": {
51
51
  "transitiveBloatThreshold": { "type": "number" },
52
- "typosquattingDistanceThreshold": { "type": "number" }
52
+ "typosquattingDistanceThreshold": { "type": "number" },
53
+ "staleReleaseDays": { "type": "number" },
54
+ "agingReleaseDays": { "type": "number" },
55
+ "lowDownloadThreshold": { "type": "number" },
56
+ "lowTrustWeightThreshold": { "type": "number" },
57
+ "mediumTrustWeightThreshold": { "type": "number" }
53
58
  }
54
59
  },
55
60
  "dashboard": {
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "riskFactors": {
21
21
  "type": "object",
22
- "required": ["daysSincePublish", "downloads", "maintainersCount", "versionCount", "recentReleaseCount", "hasRepository", "dependencyType"],
22
+ "required": ["daysSincePublish", "downloads", "maintainersCount", "versionCount", "recentReleaseCount", "hasRepository", "dependencyType", "transitiveDependencyCount", "riskyTransitiveCount"],
23
23
  "additionalProperties": false,
24
24
  "properties": {
25
25
  "daysSincePublish": { "type": ["number", "null"] },
@@ -28,7 +28,21 @@
28
28
  "versionCount": { "type": ["number", "null"] },
29
29
  "recentReleaseCount": { "type": ["number", "null"] },
30
30
  "hasRepository": { "type": "boolean" },
31
- "dependencyType": { "type": "string", "enum": ["dependencies", "devDependencies", "unknown"] }
31
+ "dependencyType": { "type": "string", "enum": ["dependencies", "devDependencies", "unknown"] },
32
+ "transitiveDependencyCount": { "type": "number" },
33
+ "riskyTransitiveCount": { "type": "number" }
34
+ }
35
+ },
36
+ "riskTransitiveDependency": {
37
+ "type": "object",
38
+ "required": ["name", "trustScore", "confidence", "reasons", "introducedByPaths"],
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "name": { "type": "string" },
42
+ "trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
43
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
44
+ "reasons": { "type": "array", "items": { "type": "string" } },
45
+ "introducedByPaths": { "type": "array", "items": { "type": "string" } }
32
46
  }
33
47
  },
34
48
  "ownershipSummary": {
@@ -159,7 +173,7 @@
159
173
  "type": "array",
160
174
  "items": {
161
175
  "type": "object",
162
- "required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "trustScore", "riskFactors", "recommendation"],
176
+ "required": ["name", "reasons", "confidence", "reasonCodes", "explanation", "trustScore", "riskFactors", "transitiveRiskScore", "riskyTransitiveDeps", "recommendation"],
163
177
  "additionalProperties": false,
164
178
  "properties": {
165
179
  "name": { "type": "string" },
@@ -170,6 +184,11 @@
170
184
  "explanation": { "type": "array", "items": { "type": "string" } },
171
185
  "trustScore": { "type": "string", "enum": ["high", "medium", "low"] },
172
186
  "riskFactors": { "$ref": "#/properties/riskFactors" },
187
+ "transitiveRiskScore": { "type": "number" },
188
+ "riskyTransitiveDeps": {
189
+ "type": "array",
190
+ "items": { "$ref": "#/properties/riskTransitiveDependency" }
191
+ },
173
192
  "recommendation": { "$ref": "#/properties/recommendation" }
174
193
  }
175
194
  }
@@ -2,8 +2,10 @@ 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
+ import type { DepBrainConfig } from "../utils/config.js";
5
6
  export interface RiskCheckOptions {
6
7
  resolvePackageMetadata?: (name: string) => Promise<PackageMetadata | null>;
8
+ thresholds?: DepBrainConfig["risk"];
7
9
  }
8
10
  export declare function findRiskDependencies(graph: DependencyGraph, options?: RiskCheckOptions): Promise<RiskDependency[]>;
9
- export declare function runRiskCheck(graph: DependencyGraph): Promise<CheckResult>;
11
+ export declare function runRiskCheck(graph: DependencyGraph, options?: RiskCheckOptions): Promise<CheckResult>;
@@ -1,13 +1,14 @@
1
1
  import { getPackageMetadata } from "../utils/npm-api.js";
2
- const TWO_YEARS_IN_DAYS = 365 * 2;
3
- const ONE_YEAR_IN_DAYS = 365;
4
2
  export async function findRiskDependencies(graph, options = {}) {
5
3
  const resolvePackageMetadata = options.resolvePackageMetadata ?? getPackageMetadata;
6
- const names = Object.keys({
4
+ const thresholds = options.thresholds;
5
+ const allNames = Object.keys({
7
6
  ...graph.dependencies,
8
- ...graph.devDependencies
7
+ ...graph.devDependencies,
8
+ ...(graph.lockPackages ?? {})
9
9
  });
10
- const results = await mapWithConcurrency(names, 8, async (name) => {
10
+ const assessments = new Map();
11
+ const results = await mapWithConcurrency(allNames, 8, async (name) => {
11
12
  const metadata = await resolvePackageMetadata(name);
12
13
  if (!metadata) {
13
14
  return null;
@@ -17,24 +18,25 @@ export async function findRiskDependencies(graph, options = {}) {
17
18
  : graph.devDependencies[name]
18
19
  ? "devDependencies"
19
20
  : "unknown";
20
- const assessment = assessRisk(metadata, dependencyType);
21
- if (!shouldReportRisk(assessment.trustScore, dependencyType)) {
22
- return null;
23
- }
21
+ const assessment = assessRisk(metadata, dependencyType, thresholds, 0);
24
22
  return {
25
23
  name,
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)
24
+ assessment
33
25
  };
34
26
  });
35
- return results
36
- .filter((item) => item !== null)
37
- .sort((left, right) => left.name.localeCompare(right.name));
27
+ for (const result of results) {
28
+ if (result) {
29
+ assessments.set(result.name, result.assessment);
30
+ }
31
+ }
32
+ const directNames = Object.keys({
33
+ ...graph.dependencies,
34
+ ...graph.devDependencies
35
+ }).sort((left, right) => left.localeCompare(right));
36
+ const risks = directNames
37
+ .map((name) => buildDirectRiskEntry(name, graph, assessments, thresholds))
38
+ .filter((item) => item !== null);
39
+ return risks.sort((left, right) => left.name.localeCompare(right.name));
38
40
  }
39
41
  async function mapWithConcurrency(items, limit, mapper) {
40
42
  const results = new Array(items.length);
@@ -50,8 +52,8 @@ async function mapWithConcurrency(items, limit, mapper) {
50
52
  await Promise.all(workers);
51
53
  return results;
52
54
  }
53
- export async function runRiskCheck(graph) {
54
- const risks = await findRiskDependencies(graph);
55
+ export async function runRiskCheck(graph, options = {}) {
56
+ const risks = await findRiskDependencies(graph, options);
55
57
  return {
56
58
  name: "risk",
57
59
  summary: `${risks.length} risky dependencies found`,
@@ -70,27 +72,140 @@ export async function runRiskCheck(graph) {
70
72
  name: item.name,
71
73
  reasons: item.reasons,
72
74
  trustScore: item.trustScore,
73
- riskFactors: item.riskFactors
75
+ riskFactors: item.riskFactors,
76
+ transitiveRiskScore: item.transitiveRiskScore,
77
+ riskyTransitiveDeps: item.riskyTransitiveDeps
78
+ }
79
+ }))
80
+ };
81
+ }
82
+ function buildDirectRiskEntry(name, graph, assessments, thresholds) {
83
+ const dependencyType = graph.dependencies[name]
84
+ ? "dependencies"
85
+ : graph.devDependencies[name]
86
+ ? "devDependencies"
87
+ : "unknown";
88
+ const selfAssessment = assessments.get(name) ?? buildUnknownAssessment(name, dependencyType);
89
+ const transitive = collectTransitiveRisks(name, graph, assessments);
90
+ const reasonCodes = [...selfAssessment.reasonCodes];
91
+ const reasons = [...selfAssessment.reasons];
92
+ const explanation = [...selfAssessment.reasons];
93
+ if (transitive.riskyTransitiveDeps.length > 0) {
94
+ reasons.push(`Introduces ${transitive.riskyTransitiveDeps.length} risky transitive dependenc${transitive.riskyTransitiveDeps.length === 1 ? "y" : "ies"}`);
95
+ reasonCodes.push("risky_transitive_dependencies");
96
+ explanation.push(`Transitive paths: ${transitive.riskyTransitiveDeps
97
+ .flatMap((item) => item.introducedByPaths)
98
+ .slice(0, 3)
99
+ .join("; ")}`);
100
+ }
101
+ if (transitive.transitiveDependencyCount > (thresholds?.transitiveBloatThreshold ?? 50)) {
102
+ reasons.push("Large transitive dependency tree");
103
+ reasonCodes.push("dependency_bloat");
104
+ explanation.push(`${name} introduces ${transitive.transitiveDependencyCount} transitive dependencies.`);
105
+ }
106
+ const transitiveRiskScore = transitive.riskyTransitiveDeps.reduce((total, item) => total + trustScoreWeight(item.trustScore), 0);
107
+ const combinedConfidence = Math.min(0.99, Math.max(selfAssessment.confidence, transitive.riskyTransitiveDeps.reduce((maxConfidence, item) => Math.max(maxConfidence, item.confidence), 0.5)));
108
+ const trustScore = combineTrustScores(selfAssessment.trustScore, transitive.highestTrustScore);
109
+ const shouldReport = shouldReportRisk(selfAssessment.trustScore, dependencyType) ||
110
+ transitive.riskyTransitiveDeps.length > 0 ||
111
+ transitive.transitiveDependencyCount > (thresholds?.transitiveBloatThreshold ?? 50);
112
+ if (!shouldReport || reasons.length === 0) {
113
+ return null;
114
+ }
115
+ return {
116
+ name,
117
+ reasons,
118
+ confidence: combinedConfidence,
119
+ reasonCodes: dedupeStrings(reasonCodes),
120
+ explanation: dedupeStrings(explanation),
121
+ trustScore,
122
+ riskFactors: {
123
+ ...selfAssessment.riskFactors,
124
+ dependencyType,
125
+ transitiveDependencyCount: transitive.transitiveDependencyCount,
126
+ riskyTransitiveCount: transitive.riskyTransitiveDeps.length
127
+ },
128
+ transitiveRiskScore,
129
+ riskyTransitiveDeps: transitive.riskyTransitiveDeps,
130
+ recommendation: buildRiskRecommendation(reasons, combinedConfidence, trustScore, transitive.riskyTransitiveDeps.length)
131
+ };
132
+ }
133
+ function collectTransitiveRisks(directName, graph, assessments) {
134
+ const visited = new Set();
135
+ const queue = (graph.lockDependencies?.[directName] ?? []).map((name) => ({
136
+ name,
137
+ path: [directName, name]
138
+ }));
139
+ const riskyByName = new Map();
140
+ let highestTrustScore = "high";
141
+ while (queue.length > 0) {
142
+ const current = queue.shift();
143
+ if (!current || visited.has(current.name)) {
144
+ continue;
145
+ }
146
+ visited.add(current.name);
147
+ const assessment = assessments.get(current.name);
148
+ if (assessment && shouldReportRisk(assessment.trustScore, assessment.riskFactors.dependencyType)) {
149
+ highestTrustScore = combineTrustScores(highestTrustScore, assessment.trustScore);
150
+ const existing = riskyByName.get(current.name);
151
+ const pathTrace = current.path.join(" -> ");
152
+ if (existing) {
153
+ existing.introducedByPaths.push(pathTrace);
154
+ }
155
+ else {
156
+ riskyByName.set(current.name, {
157
+ name: current.name,
158
+ trustScore: assessment.trustScore,
159
+ confidence: assessment.confidence,
160
+ reasons: assessment.reasons,
161
+ introducedByPaths: [pathTrace]
162
+ });
74
163
  }
164
+ }
165
+ const nextDependencies = graph.lockDependencies?.[current.name] ?? [];
166
+ for (const dependency of nextDependencies) {
167
+ if (!visited.has(dependency)) {
168
+ queue.push({
169
+ name: dependency,
170
+ path: [...current.path, dependency]
171
+ });
172
+ }
173
+ }
174
+ }
175
+ return {
176
+ transitiveDependencyCount: visited.size,
177
+ riskyTransitiveDeps: Array.from(riskyByName.values())
178
+ .map((item) => ({
179
+ ...item,
180
+ introducedByPaths: dedupeStrings(item.introducedByPaths).slice(0, 3)
75
181
  }))
182
+ .sort((left, right) => trustScoreWeight(right.trustScore) - trustScoreWeight(left.trustScore) ||
183
+ right.confidence - left.confidence ||
184
+ left.name.localeCompare(right.name)),
185
+ highestTrustScore
76
186
  };
77
187
  }
78
- function assessRisk(metadata, dependencyType) {
188
+ function assessRisk(metadata, dependencyType, thresholds, transitiveDependencyCount) {
79
189
  const reasons = [];
80
190
  const reasonCodes = [];
81
191
  let weight = 0;
82
- if (metadata.daysSincePublish !== null && metadata.daysSincePublish > TWO_YEARS_IN_DAYS) {
83
- reasons.push("No release in over 2 years");
192
+ const staleReleaseDays = thresholds?.staleReleaseDays ?? 730;
193
+ const agingReleaseDays = thresholds?.agingReleaseDays ?? 365;
194
+ const lowDownloadThreshold = thresholds?.lowDownloadThreshold ?? 1000;
195
+ const lowTrustWeightThreshold = thresholds?.lowTrustWeightThreshold ?? 6;
196
+ const mediumTrustWeightThreshold = thresholds?.mediumTrustWeightThreshold ?? 3;
197
+ if (metadata.daysSincePublish !== null && metadata.daysSincePublish > staleReleaseDays) {
198
+ reasons.push(`No release in over ${formatDays(staleReleaseDays)}`);
84
199
  reasonCodes.push("stale_release");
85
200
  weight += 3;
86
201
  }
87
202
  else if (metadata.daysSincePublish !== null &&
88
- metadata.daysSincePublish > ONE_YEAR_IN_DAYS) {
89
- reasons.push("No release in over 12 months");
203
+ metadata.daysSincePublish > agingReleaseDays) {
204
+ reasons.push(`No release in over ${formatDays(agingReleaseDays)}`);
90
205
  reasonCodes.push("aging_release");
91
206
  weight += 2;
92
207
  }
93
- if (metadata.downloads !== null && metadata.downloads < 1000) {
208
+ if (metadata.downloads !== null && metadata.downloads < lowDownloadThreshold) {
94
209
  reasons.push("Low weekly download volume");
95
210
  reasonCodes.push("low_download_volume");
96
211
  weight += 2;
@@ -118,8 +233,13 @@ function assessRisk(metadata, dependencyType) {
118
233
  weight += 1;
119
234
  }
120
235
  const confidence = reasons.length === 0 ? 0.5 : Math.min(0.99, 0.52 + weight * 0.07);
121
- const trustScore = weight >= 6 ? "low" : weight >= 3 ? "medium" : "high";
236
+ const trustScore = weight >= lowTrustWeightThreshold
237
+ ? "low"
238
+ : weight >= mediumTrustWeightThreshold
239
+ ? "medium"
240
+ : "high";
122
241
  return {
242
+ name: "",
123
243
  confidence,
124
244
  trustScore,
125
245
  reasons,
@@ -131,10 +251,41 @@ function assessRisk(metadata, dependencyType) {
131
251
  versionCount: metadata.versionCount,
132
252
  recentReleaseCount: metadata.recentReleaseCount,
133
253
  hasRepository: Boolean(metadata.repository),
134
- dependencyType
254
+ dependencyType,
255
+ transitiveDependencyCount,
256
+ riskyTransitiveCount: 0
257
+ }
258
+ };
259
+ }
260
+ function buildUnknownAssessment(name, dependencyType) {
261
+ return {
262
+ name,
263
+ confidence: 0.5,
264
+ trustScore: "high",
265
+ reasons: [],
266
+ reasonCodes: [],
267
+ riskFactors: {
268
+ daysSincePublish: null,
269
+ downloads: null,
270
+ maintainersCount: null,
271
+ versionCount: null,
272
+ recentReleaseCount: null,
273
+ hasRepository: false,
274
+ dependencyType,
275
+ transitiveDependencyCount: 0,
276
+ riskyTransitiveCount: 0
135
277
  }
136
278
  };
137
279
  }
280
+ function formatDays(days) {
281
+ if (days === 730) {
282
+ return "2 years";
283
+ }
284
+ if (days === 365) {
285
+ return "12 months";
286
+ }
287
+ return `${days} days`;
288
+ }
138
289
  function shouldReportRisk(trustScore, dependencyType) {
139
290
  if (trustScore === "high") {
140
291
  return false;
@@ -144,14 +295,33 @@ function shouldReportRisk(trustScore, dependencyType) {
144
295
  }
145
296
  return true;
146
297
  }
147
- function buildRiskRecommendation(reasons, confidence, trustScore) {
298
+ function buildRiskRecommendation(reasons, confidence, trustScore, riskyTransitiveCount) {
148
299
  return {
149
300
  action: "review",
150
- priority: trustScore === "low" || confidence >= 0.8 ? "high" : "medium",
301
+ priority: trustScore === "low" || confidence >= 0.8 || riskyTransitiveCount >= 2
302
+ ? "high"
303
+ : "medium",
151
304
  safety: "caution",
152
- summary: trustScore === "low"
153
- ? "Low trust package; review whether to replace, pin, or monitor it closely."
154
- : "Review package trust signals and decide whether to keep, replace, or monitor it.",
305
+ summary: riskyTransitiveCount > 0
306
+ ? `Review this direct dependency and its transitive chain before upgrading or keeping it.`
307
+ : trustScore === "low"
308
+ ? "Low trust package; review whether to replace, pin, or monitor it closely."
309
+ : "Review package trust signals and decide whether to keep, replace, or monitor it.",
155
310
  reasons
156
311
  };
157
312
  }
313
+ function trustScoreWeight(value) {
314
+ if (value === "low") {
315
+ return 3;
316
+ }
317
+ if (value === "medium") {
318
+ return 2;
319
+ }
320
+ return 1;
321
+ }
322
+ function combineTrustScores(left, right) {
323
+ return trustScoreWeight(left) >= trustScoreWeight(right) ? left : right;
324
+ }
325
+ function dedupeStrings(values) {
326
+ return Array.from(new Set(values));
327
+ }
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import { renderConsoleReport } from "./reporters/console.js";
4
4
  import { renderJsonReport } from "./reporters/json.js";
5
5
  import { renderMarkdownReport } from "./reporters/markdown.js";
6
6
  import { renderSarifReport } from "./reporters/sarif.js";
7
+ import { renderDashboardReport } from "./reporters/dashboard.js";
7
8
  import { defaultConfig } from "./utils/config.js";
8
9
  import { promises as fs } from "node:fs";
9
10
  import path from "node:path";
@@ -59,7 +60,9 @@ async function main() {
59
60
  ? JSON.stringify(reportData, null, 2)
60
61
  : flags.has("--sarif")
61
62
  ? renderSarifReport(reportData)
62
- : renderMarkdownReport(reportData);
63
+ : flags.has("--dashboard")
64
+ ? renderDashboardReport(reportData)
65
+ : renderMarkdownReport(reportData);
63
66
  await writeOutput(output, optionValues.get("--out"));
64
67
  return;
65
68
  }
@@ -149,6 +152,9 @@ async function main() {
149
152
  : consoleOutput;
150
153
  }
151
154
  await writeOutput(output, optionValues.get("--out"));
155
+ if (flags.has("--dashboard")) {
156
+ await writeOutput(renderDashboardReport(result), optionValues.get("--dashboard-out") ?? result.config.dashboard.outputPath);
157
+ }
152
158
  if (!result.policy.passed) {
153
159
  process.exitCode = 1;
154
160
  }
@@ -209,8 +215,8 @@ function printHelp() {
209
215
  console.log("Dependency Brain");
210
216
  console.log("");
211
217
  console.log("Usage:");
212
- console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
213
- console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--out path]");
218
+ console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
219
+ console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--dashboard] [--out path]");
214
220
  console.log(" dep-brain config [path] [--config path]");
215
221
  console.log(" dep-brain init [--out depbrain.config.json]");
216
222
  console.log(" dep-brain help");
@@ -221,6 +227,8 @@ function printHelp() {
221
227
  console.log(" --md Output Markdown report");
222
228
  console.log(" --sarif Output SARIF format for Code Scanning");
223
229
  console.log(" --top Output the ranked top issues only");
230
+ console.log(" --dashboard Write an HTML dashboard");
231
+ console.log(" --dashboard-out <path> Write dashboard HTML to a custom path");
224
232
  console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
225
233
  console.log(" --ci Apply low-noise CI defaults");
226
234
  console.log(" --config <path> Path to depbrain.config.json");
@@ -43,8 +43,17 @@ export interface RiskFactors {
43
43
  recentReleaseCount: number | null;
44
44
  hasRepository: boolean;
45
45
  dependencyType: "dependencies" | "devDependencies" | "unknown";
46
+ transitiveDependencyCount: number;
47
+ riskyTransitiveCount: number;
46
48
  }
47
49
  export type TrustScore = "high" | "medium" | "low";
50
+ export interface RiskTransitiveDependency {
51
+ name: string;
52
+ trustScore: TrustScore;
53
+ confidence: number;
54
+ reasons: string[];
55
+ introducedByPaths: string[];
56
+ }
48
57
  export interface DuplicateDependency {
49
58
  name: string;
50
59
  versions: string[];
@@ -85,6 +94,8 @@ export interface RiskDependency {
85
94
  explanation: string[];
86
95
  trustScore: TrustScore;
87
96
  riskFactors: RiskFactors;
97
+ transitiveRiskScore: number;
98
+ riskyTransitiveDeps: RiskTransitiveDependency[];
88
99
  recommendation: Recommendation;
89
100
  }
90
101
  export interface TopIssue {
@@ -133,7 +144,7 @@ export interface PackageAnalysisResult {
133
144
  topIssues: TopIssue[];
134
145
  extensions: Record<string, unknown>;
135
146
  }
136
- export declare const OUTPUT_VERSION = "1.4";
147
+ export declare const OUTPUT_VERSION = "1.5";
137
148
  export interface ScoreBreakdown {
138
149
  baseScore: number;
139
150
  duplicates: number;