dep-brain 1.2.0 → 1.3.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,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 1.3.0
6
+
7
+ - Added plugin diagnostics under `extensions.depBrain.plugins` for failed plugin loads and hook errors.
8
+ - Added built-in `license` plugin through `plugins.enabled: ["license"]`.
9
+ - Added configurable risk thresholds for stale release days, aging release days, low downloads, and trust score weights.
10
+ - Added `--dashboard` and `--dashboard-out` for static HTML dashboard generation.
11
+ - Updated starter config, schemas, README, and tests for v1.3 behavior.
12
+
13
+ ## 1.2.0
14
+
15
+ - Added `PluginManager` with `preScan`, `postScan`, and `reportHook` lifecycle support.
16
+ - Added disabled-by-default plugin config through `plugins.enabled` and `plugins.paths`.
17
+ - Added `extensions` to analysis output so plugins can enrich results without breaking schema.
18
+ - Added future config slots for risk thresholds, dashboard output, and notification webhook env names.
19
+ - Added regression coverage for plugin hooks enriching `extensions`.
20
+
5
21
  ## 1.1.0
6
22
 
7
23
  - Added `--focus` modes for targeted duplicate, unused, outdated, risk, and health analysis.
package/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
  - Highlight dependency risk signals
24
24
  - Score package trust using supply-chain metadata
25
25
  - Generate a simple project health score
26
- - Output reports in console, JSON, Markdown, SARIF, and top-issues formats
26
+ - Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
27
27
  - Gate CI with score and finding policies
28
28
  - Compare new findings against a baseline report
29
29
 
@@ -48,12 +48,14 @@ The long-term goal is not just to list problems, but to answer:
48
48
  - JSON output via `--json`
49
49
  - Markdown output via `--md`
50
50
  - SARIF output via `--sarif`
51
+ - Static HTML dashboard via `--dashboard`
51
52
  - Ranked top issues via `--top`
52
53
  - Baseline mode via `--baseline`
53
54
  - Focused analysis via `--focus`
54
55
  - Low-noise CI defaults via `--ci`
55
56
  - Starter config generation via `dep-brain init`
56
57
  - Reusable GitHub Action via `action.yml`
58
+ - Built-in `license` plugin and plugin diagnostics
57
59
  - Library entrypoint for programmatic use
58
60
 
59
61
  ## CLI Usage
@@ -73,6 +75,8 @@ npx dep-brain analyze ./path-to-project --fail-on-unused --json
73
75
  npx dep-brain analyze --md > depbrain.md
74
76
  npx dep-brain analyze --json --out depbrain.json
75
77
  npx dep-brain analyze --sarif --out depbrain.sarif
78
+ npx dep-brain analyze --dashboard
79
+ npx dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
76
80
  npx dep-brain analyze --focus duplicates
77
81
  npx dep-brain analyze --ci
78
82
  npx dep-brain analyze --baseline depbrain-baseline.json
@@ -176,6 +180,28 @@ dep-brain analyze --top
176
180
 
177
181
  Shows the highest-priority actionable findings first, including confidence and next-step guidance.
178
182
 
183
+ ## Dashboard Output
184
+
185
+ ```bash
186
+ dep-brain analyze --dashboard
187
+ dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
188
+ ```
189
+
190
+ Writes a static HTML dashboard. Default path comes from `dashboard.outputPath`.
191
+
192
+ ## Plugins
193
+
194
+ ```json
195
+ {
196
+ "plugins": {
197
+ "enabled": ["license"],
198
+ "paths": ["./depbrain-plugin.mjs"]
199
+ }
200
+ }
201
+ ```
202
+
203
+ Built-in `license` plugin adds license counts under `extensions.license`. Failed plugin loads and hook errors are reported under `extensions.depBrain.plugins`.
204
+
179
205
  ## Report From JSON
180
206
 
181
207
  ```bash
@@ -194,30 +220,6 @@ dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-ris
194
220
 
195
221
  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
222
 
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
223
  ## Config File
222
224
 
223
225
  Create a `depbrain.config.json` file in the project root:
@@ -239,12 +241,17 @@ Create a `depbrain.config.json` file in the project root:
239
241
  "maxSuggestions": 3
240
242
  },
241
243
  "plugins": {
242
- "enabled": [],
244
+ "enabled": ["license"],
243
245
  "paths": []
244
246
  },
245
247
  "risk": {
246
248
  "transitiveBloatThreshold": 50,
247
- "typosquattingDistanceThreshold": 2
249
+ "typosquattingDistanceThreshold": 2,
250
+ "staleReleaseDays": 730,
251
+ "agingReleaseDays": 365,
252
+ "lowDownloadThreshold": 1000,
253
+ "lowTrustWeightThreshold": 6,
254
+ "mediumTrustWeightThreshold": 3
248
255
  },
249
256
  "dashboard": {
250
257
  "outputPath": "depbrain-dashboard.html"
@@ -283,6 +290,11 @@ Supported sections:
283
290
  - `plugins.paths`
284
291
  - `risk.transitiveBloatThreshold`
285
292
  - `risk.typosquattingDistanceThreshold`
293
+ - `risk.staleReleaseDays`
294
+ - `risk.agingReleaseDays`
295
+ - `risk.lowDownloadThreshold`
296
+ - `risk.lowTrustWeightThreshold`
297
+ - `risk.mediumTrustWeightThreshold`
286
298
  - `dashboard.outputPath`
287
299
  - `notifications.slackWebhookEnv`
288
300
  - `notifications.discordWebhookEnv`
@@ -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": {
@@ -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,6 +1,4 @@
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
4
  const names = Object.keys({
@@ -17,7 +15,7 @@ export async function findRiskDependencies(graph, options = {}) {
17
15
  : graph.devDependencies[name]
18
16
  ? "devDependencies"
19
17
  : "unknown";
20
- const assessment = assessRisk(metadata, dependencyType);
18
+ const assessment = assessRisk(metadata, dependencyType, options.thresholds);
21
19
  if (!shouldReportRisk(assessment.trustScore, dependencyType)) {
22
20
  return null;
23
21
  }
@@ -50,8 +48,8 @@ async function mapWithConcurrency(items, limit, mapper) {
50
48
  await Promise.all(workers);
51
49
  return results;
52
50
  }
53
- export async function runRiskCheck(graph) {
54
- const risks = await findRiskDependencies(graph);
51
+ export async function runRiskCheck(graph, options = {}) {
52
+ const risks = await findRiskDependencies(graph, options);
55
53
  return {
56
54
  name: "risk",
57
55
  summary: `${risks.length} risky dependencies found`,
@@ -75,22 +73,27 @@ export async function runRiskCheck(graph) {
75
73
  }))
76
74
  };
77
75
  }
78
- function assessRisk(metadata, dependencyType) {
76
+ function assessRisk(metadata, dependencyType, thresholds) {
79
77
  const reasons = [];
80
78
  const reasonCodes = [];
81
79
  let weight = 0;
82
- if (metadata.daysSincePublish !== null && metadata.daysSincePublish > TWO_YEARS_IN_DAYS) {
83
- reasons.push("No release in over 2 years");
80
+ const staleReleaseDays = thresholds?.staleReleaseDays ?? 730;
81
+ const agingReleaseDays = thresholds?.agingReleaseDays ?? 365;
82
+ const lowDownloadThreshold = thresholds?.lowDownloadThreshold ?? 1000;
83
+ const lowTrustWeightThreshold = thresholds?.lowTrustWeightThreshold ?? 6;
84
+ const mediumTrustWeightThreshold = thresholds?.mediumTrustWeightThreshold ?? 3;
85
+ if (metadata.daysSincePublish !== null && metadata.daysSincePublish > staleReleaseDays) {
86
+ reasons.push(`No release in over ${formatDays(staleReleaseDays)}`);
84
87
  reasonCodes.push("stale_release");
85
88
  weight += 3;
86
89
  }
87
90
  else if (metadata.daysSincePublish !== null &&
88
- metadata.daysSincePublish > ONE_YEAR_IN_DAYS) {
89
- reasons.push("No release in over 12 months");
91
+ metadata.daysSincePublish > agingReleaseDays) {
92
+ reasons.push(`No release in over ${formatDays(agingReleaseDays)}`);
90
93
  reasonCodes.push("aging_release");
91
94
  weight += 2;
92
95
  }
93
- if (metadata.downloads !== null && metadata.downloads < 1000) {
96
+ if (metadata.downloads !== null && metadata.downloads < lowDownloadThreshold) {
94
97
  reasons.push("Low weekly download volume");
95
98
  reasonCodes.push("low_download_volume");
96
99
  weight += 2;
@@ -118,7 +121,11 @@ function assessRisk(metadata, dependencyType) {
118
121
  weight += 1;
119
122
  }
120
123
  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";
124
+ const trustScore = weight >= lowTrustWeightThreshold
125
+ ? "low"
126
+ : weight >= mediumTrustWeightThreshold
127
+ ? "medium"
128
+ : "high";
122
129
  return {
123
130
  confidence,
124
131
  trustScore,
@@ -135,6 +142,15 @@ function assessRisk(metadata, dependencyType) {
135
142
  }
136
143
  };
137
144
  }
145
+ function formatDays(days) {
146
+ if (days === 730) {
147
+ return "2 years";
148
+ }
149
+ if (days === 365) {
150
+ return "12 months";
151
+ }
152
+ return `${days} days`;
153
+ }
138
154
  function shouldReportRisk(trustScore, dependencyType) {
139
155
  if (trustScore === "high") {
140
156
  return false;
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");
@@ -143,7 +143,12 @@ function mergeConfig(base, overrides) {
143
143
  },
144
144
  risk: {
145
145
  transitiveBloatThreshold: overrides.risk?.transitiveBloatThreshold ?? base.risk.transitiveBloatThreshold,
146
- typosquattingDistanceThreshold: overrides.risk?.typosquattingDistanceThreshold ?? base.risk.typosquattingDistanceThreshold
146
+ typosquattingDistanceThreshold: overrides.risk?.typosquattingDistanceThreshold ?? base.risk.typosquattingDistanceThreshold,
147
+ staleReleaseDays: overrides.risk?.staleReleaseDays ?? base.risk.staleReleaseDays,
148
+ agingReleaseDays: overrides.risk?.agingReleaseDays ?? base.risk.agingReleaseDays,
149
+ lowDownloadThreshold: overrides.risk?.lowDownloadThreshold ?? base.risk.lowDownloadThreshold,
150
+ lowTrustWeightThreshold: overrides.risk?.lowTrustWeightThreshold ?? base.risk.lowTrustWeightThreshold,
151
+ mediumTrustWeightThreshold: overrides.risk?.mediumTrustWeightThreshold ?? base.risk.mediumTrustWeightThreshold
147
152
  },
148
153
  dashboard: {
149
154
  outputPath: overrides.dashboard?.outputPath ?? base.dashboard.outputPath
@@ -187,7 +192,7 @@ function evaluatePolicy(summary, config) {
187
192
  }
188
193
  async function analyzeSingleProject(rootDir, config, options = {}) {
189
194
  const context = await buildAnalysisContext(rootDir, config);
190
- const results = await runChecks(context, options.focus ?? "all");
195
+ const results = await runChecks(context, options.focus ?? "all", config);
191
196
  const issueGroups = normalizeIssues(results, config);
192
197
  const duplicates = mapDuplicateIssues(issueGroups.duplicates);
193
198
  const unused = mapUnusedIssues(issueGroups.unused);
@@ -280,7 +285,7 @@ function shouldIgnorePackage(name, bucket, config) {
280
285
  }
281
286
  });
282
287
  }
283
- async function runChecks(context, focus) {
288
+ async function runChecks(context, focus, config) {
284
289
  const checks = [
285
290
  {
286
291
  name: "duplicate",
@@ -296,7 +301,7 @@ async function runChecks(context, focus) {
296
301
  },
297
302
  {
298
303
  name: "risk",
299
- run: () => runRiskCheck(context.graph)
304
+ run: () => runRiskCheck(context.graph, { thresholds: config.risk })
300
305
  }
301
306
  ];
302
307
  const results = [];
@@ -11,10 +11,19 @@ export interface DepBrainPlugin {
11
11
  reportHook?: (result: AnalysisResult) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void;
12
12
  cliCommands?: (cli: unknown) => void;
13
13
  }
14
+ export interface PluginDiagnostic {
15
+ spec: string;
16
+ code: "load_failed" | "invalid_plugin" | "hook_failed";
17
+ message: string;
18
+ plugin?: string;
19
+ hook?: "preScan" | "postScan" | "reportHook";
20
+ }
14
21
  export declare class PluginManager {
15
22
  private readonly plugins;
23
+ private readonly diagnostics;
16
24
  private constructor();
17
25
  static load(rootDir: string, config: DepBrainConfig): Promise<PluginManager>;
18
26
  runPreScan(context: ProjectContext): Promise<void>;
19
27
  runPostScan(result: AnalysisResult): Promise<AnalysisResult>;
28
+ private attachDiagnostics;
20
29
  }
@@ -1,9 +1,12 @@
1
+ import { promises as fs } from "node:fs";
1
2
  import path from "node:path";
2
3
  import { pathToFileURL } from "node:url";
3
4
  export class PluginManager {
4
5
  plugins;
5
- constructor(plugins) {
6
+ diagnostics;
7
+ constructor(plugins, diagnostics) {
6
8
  this.plugins = plugins;
9
+ this.diagnostics = diagnostics;
7
10
  }
8
11
  static async load(rootDir, config) {
9
12
  const specs = [
@@ -11,38 +14,71 @@ export class PluginManager {
11
14
  ...config.plugins.paths
12
15
  ];
13
16
  const plugins = [];
17
+ const diagnostics = [];
14
18
  for (const spec of specs) {
15
- const plugin = await loadPlugin(rootDir, spec);
16
- if (plugin) {
17
- plugins.push(plugin);
19
+ const result = await loadPlugin(rootDir, spec);
20
+ if (result.plugin) {
21
+ plugins.push(result.plugin);
22
+ }
23
+ if (result.diagnostic) {
24
+ diagnostics.push(result.diagnostic);
18
25
  }
19
26
  }
20
- return new PluginManager(plugins);
27
+ return new PluginManager(plugins, diagnostics);
21
28
  }
22
29
  async runPreScan(context) {
23
30
  for (const plugin of this.plugins) {
24
- await plugin.preScan?.(context);
31
+ try {
32
+ await plugin.preScan?.(context);
33
+ }
34
+ catch (error) {
35
+ this.diagnostics.push(buildHookDiagnostic(plugin.name, "preScan", error));
36
+ }
25
37
  }
26
38
  }
27
39
  async runPostScan(result) {
28
40
  let current = result;
29
41
  for (const plugin of this.plugins) {
30
- const next = await plugin.postScan?.(current);
31
- if (next) {
32
- current = next;
42
+ try {
43
+ const next = await plugin.postScan?.(current);
44
+ if (next) {
45
+ current = next;
46
+ }
33
47
  }
34
- const reportSection = await plugin.reportHook?.(current);
35
- if (reportSection) {
36
- current.extensions[plugin.name] = {
37
- ...(asRecord(current.extensions[plugin.name]) ?? {}),
38
- ...reportSection
39
- };
48
+ catch (error) {
49
+ this.diagnostics.push(buildHookDiagnostic(plugin.name, "postScan", error));
40
50
  }
51
+ try {
52
+ const reportSection = await plugin.reportHook?.(current);
53
+ if (reportSection) {
54
+ current.extensions[plugin.name] = {
55
+ ...(asRecord(current.extensions[plugin.name]) ?? {}),
56
+ ...reportSection
57
+ };
58
+ }
59
+ }
60
+ catch (error) {
61
+ this.diagnostics.push(buildHookDiagnostic(plugin.name, "reportHook", error));
62
+ }
63
+ }
64
+ return this.attachDiagnostics(current);
65
+ }
66
+ attachDiagnostics(result) {
67
+ if (this.diagnostics.length === 0) {
68
+ return result;
41
69
  }
42
- return current;
70
+ result.extensions.depBrain = {
71
+ ...(asRecord(result.extensions.depBrain) ?? {}),
72
+ plugins: this.diagnostics
73
+ };
74
+ return result;
43
75
  }
44
76
  }
45
77
  async function loadPlugin(rootDir, spec) {
78
+ const builtIn = getBuiltInPlugin(spec);
79
+ if (builtIn) {
80
+ return { plugin: builtIn };
81
+ }
46
82
  try {
47
83
  const resolved = spec.startsWith(".") || path.isAbsolute(spec)
48
84
  ? path.resolve(rootDir, spec)
@@ -51,11 +87,99 @@ async function loadPlugin(rootDir, spec) {
51
87
  const mod = await import(moduleUrl);
52
88
  const exported = mod.default ?? mod.plugin ?? mod;
53
89
  const candidate = typeof exported === "function" ? new exported() : exported;
54
- return isPlugin(candidate) ? candidate : null;
90
+ if (isPlugin(candidate)) {
91
+ return { plugin: candidate };
92
+ }
93
+ return {
94
+ plugin: null,
95
+ diagnostic: {
96
+ spec,
97
+ code: "invalid_plugin",
98
+ message: "Plugin must export an object with a string name."
99
+ }
100
+ };
55
101
  }
56
- catch {
102
+ catch (error) {
103
+ return {
104
+ plugin: null,
105
+ diagnostic: {
106
+ spec,
107
+ code: "load_failed",
108
+ message: error instanceof Error ? error.message : String(error)
109
+ }
110
+ };
111
+ }
112
+ }
113
+ function getBuiltInPlugin(spec) {
114
+ if (spec !== "license" && spec !== "dep-brain-plugin-license") {
57
115
  return null;
58
116
  }
117
+ return {
118
+ name: "license",
119
+ reportHook: async (result) => {
120
+ const packages = await collectLicensePackages(result.rootDir);
121
+ const licenses = packages.reduce((acc, item) => {
122
+ acc[item.license] = (acc[item.license] ?? 0) + 1;
123
+ return acc;
124
+ }, {});
125
+ return {
126
+ summary: {
127
+ total: packages.length,
128
+ unknown: packages.filter((item) => item.license === "UNKNOWN").length,
129
+ licenses
130
+ },
131
+ packages
132
+ };
133
+ }
134
+ };
135
+ }
136
+ async function collectLicensePackages(rootDir) {
137
+ const raw = await fs.readFile(path.join(rootDir, "package.json"), "utf8");
138
+ const pkg = JSON.parse(raw);
139
+ const names = Object.keys({
140
+ ...(pkg.dependencies ?? {}),
141
+ ...(pkg.devDependencies ?? {})
142
+ }).sort();
143
+ return Promise.all(names.map(async (name) => ({
144
+ name,
145
+ license: await readPackageLicense(rootDir, name)
146
+ })));
147
+ }
148
+ async function readPackageLicense(rootDir, name) {
149
+ try {
150
+ const raw = await fs.readFile(path.join(rootDir, "node_modules", name, "package.json"), "utf8");
151
+ const pkg = JSON.parse(raw);
152
+ if (typeof pkg.license === "string" && pkg.license.trim().length > 0) {
153
+ return pkg.license;
154
+ }
155
+ if (Array.isArray(pkg.licenses) && pkg.licenses.length > 0) {
156
+ const licenses = pkg.licenses
157
+ .map((item) => {
158
+ if (typeof item === "string") {
159
+ return item;
160
+ }
161
+ if (item && typeof item === "object" && typeof item.type === "string") {
162
+ return item.type;
163
+ }
164
+ return null;
165
+ })
166
+ .filter((item) => Boolean(item));
167
+ return licenses.length > 0 ? licenses.join(", ") : "UNKNOWN";
168
+ }
169
+ }
170
+ catch {
171
+ return "UNKNOWN";
172
+ }
173
+ return "UNKNOWN";
174
+ }
175
+ function buildHookDiagnostic(plugin, hook, error) {
176
+ return {
177
+ spec: plugin,
178
+ plugin,
179
+ hook,
180
+ code: "hook_failed",
181
+ message: error instanceof Error ? error.message : String(error)
182
+ };
59
183
  }
60
184
  function isPlugin(value) {
61
185
  return Boolean(value &&
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { analyzeProject } from "./core/analyzer.js";
2
2
  export type { AnalysisOptions, AnalysisFocus, AnalysisResult, DepBrainBaseline, 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 { PluginManager } from "./core/plugin-manager.js";
5
- export type { DepBrainPlugin, ProjectContext } from "./core/plugin-manager.js";
5
+ export type { DepBrainPlugin, PluginDiagnostic, ProjectContext } from "./core/plugin-manager.js";
6
6
  export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
7
7
  export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
8
8
  export type { WorkspacePackage } from "./utils/workspaces.js";
@@ -0,0 +1,2 @@
1
+ import type { AnalysisResult } from "../core/analyzer.js";
2
+ export declare function renderDashboardReport(result: AnalysisResult): string;
@@ -0,0 +1,84 @@
1
+ export function renderDashboardReport(result) {
2
+ const topIssues = result.topIssues.map(renderTopIssue).join("");
3
+ const suggestions = result.suggestions.map((item) => `<li>${escapeHtml(item)}</li>`).join("");
4
+ return [
5
+ "<!doctype html>",
6
+ '<html lang="en">',
7
+ "<head>",
8
+ '<meta charset="utf-8">',
9
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
10
+ "<title>Dependency Brain Dashboard</title>",
11
+ "<style>",
12
+ "body{font-family:Arial,sans-serif;margin:0;color:#172033;background:#f6f8fb}",
13
+ "main{max-width:1120px;margin:0 auto;padding:32px}",
14
+ "header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}",
15
+ "h1{font-size:28px;margin:0 0 8px}",
16
+ "h2{font-size:18px;margin:0 0 12px}",
17
+ ".muted{color:#637083;font-size:13px}",
18
+ ".score{font-size:48px;font-weight:700}",
19
+ ".grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:20px}",
20
+ ".panel{background:#fff;border:1px solid #dce3ee;border-radius:8px;padding:16px}",
21
+ ".metric{font-size:28px;font-weight:700;margin-top:4px}",
22
+ ".pass{color:#167a43}.fail{color:#b42318}",
23
+ "ol,ul{margin:0;padding-left:20px}",
24
+ "li{margin:8px 0}",
25
+ ".issue{margin-bottom:10px}",
26
+ ".kind{font-size:12px;text-transform:uppercase;color:#637083}",
27
+ "@media(max-width:760px){main{padding:20px}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}header{display:block}.score{font-size:40px}}",
28
+ "</style>",
29
+ "</head>",
30
+ "<body>",
31
+ "<main>",
32
+ "<header>",
33
+ "<div>",
34
+ "<h1>Dependency Brain Dashboard</h1>",
35
+ `<div class="muted">${escapeHtml(result.rootDir)}</div>`,
36
+ "</div>",
37
+ `<div class="${result.policy.passed ? "pass" : "fail"} score">${result.score}/100</div>`,
38
+ "</header>",
39
+ '<section class="grid">',
40
+ renderMetric("Duplicates", result.duplicates.length),
41
+ renderMetric("Unused", result.unused.length),
42
+ renderMetric("Outdated", result.outdated.length),
43
+ renderMetric("Risks", result.risks.length),
44
+ "</section>",
45
+ '<section class="panel">',
46
+ "<h2>Policy</h2>",
47
+ `<p class="${result.policy.passed ? "pass" : "fail"}">${result.policy.passed ? "Passed" : "Failed"}</p>`,
48
+ result.policy.reasons.length > 0
49
+ ? `<ul>${result.policy.reasons.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`
50
+ : '<p class="muted">No policy failures.</p>',
51
+ "</section>",
52
+ '<section class="panel">',
53
+ "<h2>Top Issues</h2>",
54
+ topIssues.length > 0 ? `<ol>${topIssues}</ol>` : '<p class="muted">No actionable issues found.</p>',
55
+ "</section>",
56
+ '<section class="panel">',
57
+ "<h2>Suggestions</h2>",
58
+ suggestions.length > 0 ? `<ul>${suggestions}</ul>` : '<p class="muted">No suggestions.</p>',
59
+ "</section>",
60
+ "</main>",
61
+ "</body>",
62
+ "</html>"
63
+ ].join("\n");
64
+ }
65
+ function renderMetric(label, value) {
66
+ return `<div class="panel"><div class="muted">${escapeHtml(label)}</div><div class="metric">${value}</div></div>`;
67
+ }
68
+ function renderTopIssue(item) {
69
+ return [
70
+ '<li class="issue">',
71
+ `<div class="kind">${escapeHtml(item.kind)} ${escapeHtml(item.priority)}</div>`,
72
+ `<strong>${escapeHtml(item.name)}</strong>`,
73
+ `<div>${escapeHtml(item.recommendation.summary)}</div>`,
74
+ "</li>"
75
+ ].join("");
76
+ }
77
+ function escapeHtml(value) {
78
+ return value
79
+ .replace(/&/g, "&amp;")
80
+ .replace(/</g, "&lt;")
81
+ .replace(/>/g, "&gt;")
82
+ .replace(/"/g, "&quot;")
83
+ .replace(/'/g, "&#39;");
84
+ }
@@ -26,6 +26,11 @@ export interface DepBrainConfig {
26
26
  risk: {
27
27
  transitiveBloatThreshold: number;
28
28
  typosquattingDistanceThreshold: number;
29
+ staleReleaseDays: number;
30
+ agingReleaseDays: number;
31
+ lowDownloadThreshold: number;
32
+ lowTrustWeightThreshold: number;
33
+ mediumTrustWeightThreshold: number;
29
34
  };
30
35
  dashboard: {
31
36
  outputPath: string;
@@ -27,7 +27,12 @@ export const defaultConfig = {
27
27
  },
28
28
  risk: {
29
29
  transitiveBloatThreshold: 50,
30
- typosquattingDistanceThreshold: 2
30
+ typosquattingDistanceThreshold: 2,
31
+ staleReleaseDays: 730,
32
+ agingReleaseDays: 365,
33
+ lowDownloadThreshold: 1000,
34
+ lowTrustWeightThreshold: 6,
35
+ mediumTrustWeightThreshold: 3
31
36
  },
32
37
  dashboard: {
33
38
  outputPath: "depbrain-dashboard.html"
@@ -84,7 +89,12 @@ function normalizeConfig(loaded) {
84
89
  },
85
90
  risk: {
86
91
  transitiveBloatThreshold: normalizeNumber(loaded.risk?.transitiveBloatThreshold, defaultConfig.risk.transitiveBloatThreshold),
87
- typosquattingDistanceThreshold: normalizeNumber(loaded.risk?.typosquattingDistanceThreshold, defaultConfig.risk.typosquattingDistanceThreshold)
92
+ typosquattingDistanceThreshold: normalizeNumber(loaded.risk?.typosquattingDistanceThreshold, defaultConfig.risk.typosquattingDistanceThreshold),
93
+ staleReleaseDays: normalizeNumber(loaded.risk?.staleReleaseDays, defaultConfig.risk.staleReleaseDays),
94
+ agingReleaseDays: normalizeNumber(loaded.risk?.agingReleaseDays, defaultConfig.risk.agingReleaseDays),
95
+ lowDownloadThreshold: normalizeNumber(loaded.risk?.lowDownloadThreshold, defaultConfig.risk.lowDownloadThreshold),
96
+ lowTrustWeightThreshold: normalizeNumber(loaded.risk?.lowTrustWeightThreshold, defaultConfig.risk.lowTrustWeightThreshold),
97
+ mediumTrustWeightThreshold: normalizeNumber(loaded.risk?.mediumTrustWeightThreshold, defaultConfig.risk.mediumTrustWeightThreshold)
88
98
  },
89
99
  dashboard: {
90
100
  outputPath: normalizeString(loaded.dashboard?.outputPath, defaultConfig.dashboard.outputPath)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "CLI and library for explainable dependency intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",