dep-brain 1.0.2 → 1.2.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,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 1.1.0
6
+
7
+ - Added `--focus` modes for targeted duplicate, unused, outdated, risk, and health analysis.
8
+ - Added `--ci` for low-noise CI defaults focused on duplicate and runtime risk enforcement.
9
+ - Added `dep-brain init` to generate a starter `depbrain.config.json`.
10
+ - Introduced capped health score deductions so large outdated/risk counts do not automatically collapse project health to `0/100`.
11
+ - Added GitHub Action inputs for `focus` and `ci`.
12
+ - Added regression coverage for focused analysis and capped scoring.
13
+
5
14
  ## 1.0.2
6
15
 
7
16
  - Treated npm `overrides` entries as intentional version pins so direct override packages are not flagged as unused.
package/README.md CHANGED
@@ -50,6 +50,9 @@ The long-term goal is not just to list problems, but to answer:
50
50
  - SARIF output via `--sarif`
51
51
  - Ranked top issues via `--top`
52
52
  - Baseline mode via `--baseline`
53
+ - Focused analysis via `--focus`
54
+ - Low-noise CI defaults via `--ci`
55
+ - Starter config generation via `dep-brain init`
53
56
  - Reusable GitHub Action via `action.yml`
54
57
  - Library entrypoint for programmatic use
55
58
 
@@ -70,10 +73,13 @@ npx dep-brain analyze ./path-to-project --fail-on-unused --json
70
73
  npx dep-brain analyze --md > depbrain.md
71
74
  npx dep-brain analyze --json --out depbrain.json
72
75
  npx dep-brain analyze --sarif --out depbrain.sarif
76
+ npx dep-brain analyze --focus duplicates
77
+ npx dep-brain analyze --ci
73
78
  npx dep-brain analyze --baseline depbrain-baseline.json
74
79
  npx dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-risks
75
80
  npx dep-brain report --from depbrain.json --md --out depbrain.md
76
81
 
82
+ dep-brain init
77
83
  dep-brain config
78
84
  dep-brain config --config depbrain.config.json
79
85
 
@@ -82,6 +88,34 @@ dep-brain analyze --help
82
88
  dep-brain --version
83
89
  ```
84
90
 
91
+ ## Focus Modes
92
+
93
+ Use `--focus` when you want a targeted signal instead of a full report:
94
+
95
+ ```bash
96
+ dep-brain analyze --focus duplicates
97
+ dep-brain analyze --focus health
98
+ dep-brain analyze --focus risks
99
+ ```
100
+
101
+ Supported values are `all`, `health`, `duplicates`, `unused`, `outdated`, and `risks`.
102
+
103
+ ## CI Preset
104
+
105
+ ```bash
106
+ dep-brain analyze --ci
107
+ ```
108
+
109
+ The CI preset applies low-noise defaults: a minimum score of `70`, failure on duplicates, and failure on risky dependencies.
110
+
111
+ ## Config Init
112
+
113
+ ```bash
114
+ dep-brain init
115
+ ```
116
+
117
+ Creates a starter `depbrain.config.json` with practical defaults for CI and common TypeScript/NestJS tooling.
118
+
85
119
  ## Workspaces
86
120
 
87
121
  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.
@@ -204,6 +238,21 @@ Create a `depbrain.config.json` file in the project root:
204
238
  "report": {
205
239
  "maxSuggestions": 3
206
240
  },
241
+ "plugins": {
242
+ "enabled": [],
243
+ "paths": []
244
+ },
245
+ "risk": {
246
+ "transitiveBloatThreshold": 50,
247
+ "typosquattingDistanceThreshold": 2
248
+ },
249
+ "dashboard": {
250
+ "outputPath": "depbrain-dashboard.html"
251
+ },
252
+ "notifications": {
253
+ "slackWebhookEnv": "DEPBRAIN_SLACK_WEBHOOK_URL",
254
+ "discordWebhookEnv": "DEPBRAIN_DISCORD_WEBHOOK_URL"
255
+ },
207
256
  "scoring": {
208
257
  "duplicateWeight": 5,
209
258
  "outdatedWeight": 1,
@@ -230,6 +279,13 @@ Supported sections:
230
279
  - `policy.failOnOutdated`
231
280
  - `policy.failOnRisks`
232
281
  - `report.maxSuggestions`
282
+ - `plugins.enabled`
283
+ - `plugins.paths`
284
+ - `risk.transitiveBloatThreshold`
285
+ - `risk.typosquattingDistanceThreshold`
286
+ - `dashboard.outputPath`
287
+ - `notifications.slackWebhookEnv`
288
+ - `notifications.discordWebhookEnv`
233
289
  - `scoring.duplicateWeight`
234
290
  - `scoring.outdatedWeight`
235
291
  - `scoring.unusedWeight`
package/action.yml CHANGED
@@ -23,6 +23,14 @@ inputs:
23
23
  description: Optional baseline JSON report used to ignore existing findings.
24
24
  required: false
25
25
  default: ""
26
+ focus:
27
+ description: Analysis focus. Use all, health, duplicates, unused, outdated, or risks.
28
+ required: false
29
+ default: "all"
30
+ ci:
31
+ description: Apply low-noise CI defaults.
32
+ required: false
33
+ default: "false"
26
34
  min-score:
27
35
  description: Minimum project health score required to pass.
28
36
  required: false
@@ -55,6 +63,8 @@ runs:
55
63
  INPUT_OUT: ${{ inputs.out }}
56
64
  INPUT_CONFIG: ${{ inputs.config }}
57
65
  INPUT_BASELINE: ${{ inputs.baseline }}
66
+ INPUT_FOCUS: ${{ inputs.focus }}
67
+ INPUT_CI: ${{ inputs.ci }}
58
68
  INPUT_MIN_SCORE: ${{ inputs.min-score }}
59
69
  INPUT_FAIL_ON_UNUSED: ${{ inputs.fail-on-unused }}
60
70
  INPUT_FAIL_ON_OUTDATED: ${{ inputs.fail-on-outdated }}
@@ -89,6 +99,14 @@ runs:
89
99
  args+=("--baseline" "$INPUT_BASELINE")
90
100
  fi
91
101
 
102
+ if [ "$INPUT_FOCUS" != "all" ]; then
103
+ args+=("--focus" "$INPUT_FOCUS")
104
+ fi
105
+
106
+ if [ "$INPUT_CI" = "true" ]; then
107
+ args+=("--ci")
108
+ fi
109
+
92
110
  if [ -n "$INPUT_MIN_SCORE" ]; then
93
111
  args+=("--min-score" "$INPUT_MIN_SCORE")
94
112
  fi
@@ -19,6 +19,21 @@
19
19
  "report": {
20
20
  "maxSuggestions": 5
21
21
  },
22
+ "plugins": {
23
+ "enabled": [],
24
+ "paths": []
25
+ },
26
+ "risk": {
27
+ "transitiveBloatThreshold": 50,
28
+ "typosquattingDistanceThreshold": 2
29
+ },
30
+ "dashboard": {
31
+ "outputPath": "depbrain-dashboard.html"
32
+ },
33
+ "notifications": {
34
+ "slackWebhookEnv": "DEPBRAIN_SLACK_WEBHOOK_URL",
35
+ "discordWebhookEnv": "DEPBRAIN_DISCORD_WEBHOOK_URL"
36
+ },
22
37
  "scoring": {
23
38
  "duplicateWeight": 5,
24
39
  "outdatedWeight": 3,
@@ -36,6 +36,37 @@
36
36
  "maxSuggestions": { "type": "number" }
37
37
  }
38
38
  },
39
+ "plugins": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "enabled": { "type": "array", "items": { "type": "string" } },
44
+ "paths": { "type": "array", "items": { "type": "string" } }
45
+ }
46
+ },
47
+ "risk": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "properties": {
51
+ "transitiveBloatThreshold": { "type": "number" },
52
+ "typosquattingDistanceThreshold": { "type": "number" }
53
+ }
54
+ },
55
+ "dashboard": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "properties": {
59
+ "outputPath": { "type": "string" }
60
+ }
61
+ },
62
+ "notifications": {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "properties": {
66
+ "slackWebhookEnv": { "type": "string" },
67
+ "discordWebhookEnv": { "type": "string" }
68
+ }
69
+ },
39
70
  "scoring": {
40
71
  "type": "object",
41
72
  "additionalProperties": false,
@@ -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", "ownershipSummary", "duplicates", "unused", "outdated", "risks", "suggestions", "topIssues", "config"],
5
+ "required": ["outputVersion", "rootDir", "score", "scoreBreakdown", "policy", "ownershipSummary", "duplicates", "unused", "outdated", "risks", "suggestions", "topIssues", "extensions", "config"],
6
6
  "additionalProperties": false,
7
7
  "properties": {
8
8
  "recommendation": {
@@ -175,6 +175,10 @@
175
175
  }
176
176
  },
177
177
  "suggestions": { "type": "array", "items": { "type": "string" } },
178
+ "extensions": {
179
+ "type": "object",
180
+ "additionalProperties": true
181
+ },
178
182
  "topIssues": {
179
183
  "type": "array",
180
184
  "items": {
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 { defaultConfig } from "./utils/config.js";
7
8
  import { promises as fs } from "node:fs";
8
9
  import path from "node:path";
9
10
  async function main() {
@@ -91,6 +92,22 @@ async function main() {
91
92
  return;
92
93
  }
93
94
  }
95
+ if (command === "init") {
96
+ try {
97
+ const outputPath = optionValues.get("--out") ?? "depbrain.config.json";
98
+ const resolvedOut = resolveUserPath(outputPath);
99
+ const config = buildStarterConfig();
100
+ await fs.writeFile(resolvedOut, `${JSON.stringify(config, null, 2)}\n`, "utf8");
101
+ console.log(`Created ${path.relative(process.cwd(), resolvedOut) || outputPath}`);
102
+ return;
103
+ }
104
+ catch (error) {
105
+ console.error("Failed to create config.");
106
+ console.error(error);
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+ }
94
111
  console.error(`Unknown command: ${sanitizeForLog(command)}`);
95
112
  printHelp();
96
113
  process.exitCode = 1;
@@ -108,7 +125,8 @@ async function main() {
108
125
  rootDir: targetPath,
109
126
  configPath: optionValues.get("--config"),
110
127
  config: cliConfig,
111
- baseline
128
+ baseline,
129
+ focus: parseFocus(optionValues.get("--focus"))
112
130
  });
113
131
  let output;
114
132
  if (flags.has("--json")) {
@@ -169,6 +187,11 @@ function buildCliConfig(flags, optionValues) {
169
187
  if (flags.has("--fail-on-unused")) {
170
188
  policy.failOnUnused = true;
171
189
  }
190
+ if (flags.has("--ci")) {
191
+ policy.minScore = policy.minScore ?? 70;
192
+ policy.failOnDuplicates = true;
193
+ policy.failOnRisks = true;
194
+ }
172
195
  return {
173
196
  policy
174
197
  };
@@ -186,9 +209,10 @@ function printHelp() {
186
209
  console.log("Dependency Brain");
187
210
  console.log("");
188
211
  console.log("Usage:");
189
- console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
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]");
190
213
  console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--out path]");
191
214
  console.log(" dep-brain config [path] [--config path]");
215
+ console.log(" dep-brain init [--out depbrain.config.json]");
192
216
  console.log(" dep-brain help");
193
217
  console.log(" dep-brain --version");
194
218
  console.log("");
@@ -197,6 +221,8 @@ function printHelp() {
197
221
  console.log(" --md Output Markdown report");
198
222
  console.log(" --sarif Output SARIF format for Code Scanning");
199
223
  console.log(" --top Output the ranked top issues only");
224
+ console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
225
+ console.log(" --ci Apply low-noise CI defaults");
200
226
  console.log(" --config <path> Path to depbrain.config.json");
201
227
  console.log(" --baseline <path> Ignore findings already present in a baseline JSON report");
202
228
  console.log(" --from <file> Read analysis JSON from file");
@@ -209,6 +235,45 @@ function printHelp() {
209
235
  console.log(" --help Show this help output");
210
236
  console.log(" --version Show CLI version");
211
237
  }
238
+ function parseFocus(value) {
239
+ if (value === "duplicates" ||
240
+ value === "unused" ||
241
+ value === "outdated" ||
242
+ value === "risks" ||
243
+ value === "health" ||
244
+ value === "all") {
245
+ return value;
246
+ }
247
+ return "all";
248
+ }
249
+ function buildStarterConfig() {
250
+ return {
251
+ ...defaultConfig,
252
+ ignore: {
253
+ ...defaultConfig.ignore,
254
+ unused: [
255
+ "@nestjs/platform-express",
256
+ "reflect-metadata",
257
+ "source-map-support",
258
+ "ts-loader",
259
+ "ts-node",
260
+ "tsconfig-paths"
261
+ ]
262
+ },
263
+ policy: {
264
+ ...defaultConfig.policy,
265
+ minScore: 70,
266
+ failOnDuplicates: true,
267
+ failOnRisks: true
268
+ },
269
+ scoring: {
270
+ duplicateWeight: 8,
271
+ outdatedWeight: 1,
272
+ unusedWeight: 2,
273
+ riskWeight: 4
274
+ }
275
+ };
276
+ }
212
277
  async function loadPackageVersion() {
213
278
  try {
214
279
  const pkgPath = new URL("../package.json", import.meta.url);
@@ -4,7 +4,9 @@ export interface AnalysisOptions {
4
4
  configPath?: string;
5
5
  config?: DepBrainConfigOverrides;
6
6
  baseline?: DepBrainBaseline;
7
+ focus?: AnalysisFocus;
7
8
  }
9
+ export type AnalysisFocus = "all" | "duplicates" | "unused" | "outdated" | "risks" | "health";
8
10
  export interface DepBrainBaseline {
9
11
  duplicates?: Array<Partial<Pick<DuplicateDependency, "name">>>;
10
12
  unused?: Array<Partial<Pick<UnusedDependency, "name" | "section" | "package">>>;
@@ -108,6 +110,7 @@ export interface AnalysisResult {
108
110
  risks: RiskDependency[];
109
111
  suggestions: string[];
110
112
  topIssues: TopIssue[];
113
+ extensions: Record<string, unknown>;
111
114
  config: DepBrainConfig;
112
115
  packages?: PackageAnalysisResult[];
113
116
  }
@@ -128,6 +131,7 @@ export interface PackageAnalysisResult {
128
131
  risks: RiskDependency[];
129
132
  suggestions: string[];
130
133
  topIssues: TopIssue[];
134
+ extensions: Record<string, unknown>;
131
135
  }
132
136
  export declare const OUTPUT_VERSION = "1.4";
133
137
  export interface ScoreBreakdown {
@@ -6,27 +6,34 @@ import { runUnusedCheck } from "../checks/unused.js";
6
6
  import { loadDepBrainConfig } from "../utils/config.js";
7
7
  import { findWorkspacePackages } from "../utils/workspaces.js";
8
8
  import { buildDependencyGraph } from "./graph-builder.js";
9
- import { calculateHealthScore } from "./scorer.js";
9
+ import { PluginManager } from "./plugin-manager.js";
10
+ import { calculateHealthScore, calculateScoreDeductions } from "./scorer.js";
10
11
  import { buildAnalysisContext } from "./context.js";
11
12
  export const OUTPUT_VERSION = "1.4";
12
13
  export async function analyzeProject(options = {}) {
13
14
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
14
15
  const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
15
16
  const config = mergeConfig(loadedConfig, options.config);
17
+ const focus = options.focus ?? "all";
18
+ const plugins = await PluginManager.load(rootDir, config);
19
+ await plugins.runPreScan({ rootDir, config });
16
20
  const workspaces = await findWorkspacePackages(rootDir);
17
21
  if (workspaces.length === 0) {
18
- return analyzeSingleProject(rootDir, config, {
19
- baseline: options.baseline
22
+ const result = await analyzeSingleProject(rootDir, config, {
23
+ baseline: options.baseline,
24
+ focus
20
25
  });
26
+ return plugins.runPostScan(result);
21
27
  }
22
28
  const rootGraph = await buildDependencyGraph(rootDir);
23
- const duplicateCheck = await runDuplicateCheck(rootGraph);
24
- const filteredDuplicateIssues = filterIssues(duplicateCheck.issues, "duplicates", config);
25
- const duplicates = mapDuplicateIssues(filteredDuplicateIssues);
29
+ const duplicates = shouldRunCheck("duplicate", focus)
30
+ ? mapDuplicateIssues(filterIssues((await runDuplicateCheck(rootGraph)).issues, "duplicates", config))
31
+ : [];
26
32
  const packages = [];
27
33
  for (const workspace of workspaces) {
28
34
  const result = await analyzeSingleProject(workspace.rootDir, config, {
29
- packageName: workspace.name
35
+ packageName: workspace.name,
36
+ focus
30
37
  });
31
38
  packages.push({ ...result, name: workspace.name });
32
39
  }
@@ -76,7 +83,7 @@ export async function analyzeProject(options = {}) {
76
83
  outdated: outdated.length,
77
84
  risks: risks.length
78
85
  }, config);
79
- return {
86
+ const result = {
80
87
  outputVersion: OUTPUT_VERSION,
81
88
  rootDir,
82
89
  score,
@@ -99,9 +106,11 @@ export async function analyzeProject(options = {}) {
99
106
  outdated,
100
107
  risks
101
108
  }),
109
+ extensions: {},
102
110
  config,
103
111
  packages
104
112
  };
113
+ return plugins.runPostScan(result);
105
114
  }
106
115
  function mergeConfig(base, overrides) {
107
116
  if (!overrides) {
@@ -128,6 +137,21 @@ function mergeConfig(base, overrides) {
128
137
  report: {
129
138
  maxSuggestions: overrides.report?.maxSuggestions ?? base.report.maxSuggestions
130
139
  },
140
+ plugins: {
141
+ enabled: overrides.plugins?.enabled ?? base.plugins.enabled,
142
+ paths: overrides.plugins?.paths ?? base.plugins.paths
143
+ },
144
+ risk: {
145
+ transitiveBloatThreshold: overrides.risk?.transitiveBloatThreshold ?? base.risk.transitiveBloatThreshold,
146
+ typosquattingDistanceThreshold: overrides.risk?.typosquattingDistanceThreshold ?? base.risk.typosquattingDistanceThreshold
147
+ },
148
+ dashboard: {
149
+ outputPath: overrides.dashboard?.outputPath ?? base.dashboard.outputPath
150
+ },
151
+ notifications: {
152
+ slackWebhookEnv: overrides.notifications?.slackWebhookEnv ?? base.notifications.slackWebhookEnv,
153
+ discordWebhookEnv: overrides.notifications?.discordWebhookEnv ?? base.notifications.discordWebhookEnv
154
+ },
131
155
  scoring: {
132
156
  duplicateWeight: overrides.scoring?.duplicateWeight ?? base.scoring.duplicateWeight,
133
157
  outdatedWeight: overrides.scoring?.outdatedWeight ?? base.scoring.outdatedWeight,
@@ -163,7 +187,7 @@ function evaluatePolicy(summary, config) {
163
187
  }
164
188
  async function analyzeSingleProject(rootDir, config, options = {}) {
165
189
  const context = await buildAnalysisContext(rootDir, config);
166
- const results = await runChecks(context);
190
+ const results = await runChecks(context, options.focus ?? "all");
167
191
  const issueGroups = normalizeIssues(results, config);
168
192
  const duplicates = mapDuplicateIssues(issueGroups.duplicates);
169
193
  const unused = mapUnusedIssues(issueGroups.unused);
@@ -235,6 +259,7 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
235
259
  outdated: baselineFiltered.outdated,
236
260
  risks: baselineFiltered.risks
237
261
  }),
262
+ extensions: {},
238
263
  config
239
264
  };
240
265
  }
@@ -255,7 +280,7 @@ function shouldIgnorePackage(name, bucket, config) {
255
280
  }
256
281
  });
257
282
  }
258
- async function runChecks(context) {
283
+ async function runChecks(context, focus) {
259
284
  const checks = [
260
285
  {
261
286
  name: "duplicate",
@@ -276,10 +301,21 @@ async function runChecks(context) {
276
301
  ];
277
302
  const results = [];
278
303
  for (const check of checks) {
279
- results.push(await check.run());
304
+ if (shouldRunCheck(check.name, focus)) {
305
+ results.push(await check.run());
306
+ }
280
307
  }
281
308
  return results;
282
309
  }
310
+ function shouldRunCheck(checkName, focus) {
311
+ if (focus === "all" || focus === "health") {
312
+ return true;
313
+ }
314
+ return ((focus === "duplicates" && checkName === "duplicate") ||
315
+ (focus === "unused" && checkName === "unused") ||
316
+ (focus === "outdated" && checkName === "outdated") ||
317
+ (focus === "risks" && checkName === "risk"));
318
+ }
283
319
  function normalizeIssues(results, config) {
284
320
  const map = new Map();
285
321
  for (const result of results) {
@@ -603,12 +639,22 @@ function normalizeWorkspaceUsage(value) {
603
639
  .filter((entry) => entry !== null);
604
640
  }
605
641
  function buildScoreBreakdown(counts, config) {
642
+ const deductions = calculateScoreDeductions({
643
+ duplicates: counts.duplicates,
644
+ outdated: counts.outdated,
645
+ unused: counts.unused,
646
+ risks: counts.risks,
647
+ duplicateWeight: config.scoring.duplicateWeight,
648
+ outdatedWeight: config.scoring.outdatedWeight,
649
+ unusedWeight: config.scoring.unusedWeight,
650
+ riskWeight: config.scoring.riskWeight
651
+ });
606
652
  return {
607
- baseScore: 100,
608
- duplicates: counts.duplicates * config.scoring.duplicateWeight,
609
- outdated: counts.outdated * config.scoring.outdatedWeight,
610
- unused: counts.unused * config.scoring.unusedWeight,
611
- risks: counts.risks * config.scoring.riskWeight,
653
+ baseScore: deductions.baseScore,
654
+ duplicates: deductions.duplicates,
655
+ outdated: deductions.outdated,
656
+ unused: deductions.unused,
657
+ risks: deductions.risks,
612
658
  weights: {
613
659
  duplicateWeight: config.scoring.duplicateWeight,
614
660
  outdatedWeight: config.scoring.outdatedWeight,
@@ -0,0 +1,20 @@
1
+ import type { AnalysisResult } from "./analyzer.js";
2
+ import type { DepBrainConfig } from "../utils/config.js";
3
+ export interface ProjectContext {
4
+ rootDir: string;
5
+ config: DepBrainConfig;
6
+ }
7
+ export interface DepBrainPlugin {
8
+ name: string;
9
+ preScan?: (context: ProjectContext) => Promise<void> | void;
10
+ postScan?: (result: AnalysisResult) => Promise<AnalysisResult | void> | AnalysisResult | void;
11
+ reportHook?: (result: AnalysisResult) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void;
12
+ cliCommands?: (cli: unknown) => void;
13
+ }
14
+ export declare class PluginManager {
15
+ private readonly plugins;
16
+ private constructor();
17
+ static load(rootDir: string, config: DepBrainConfig): Promise<PluginManager>;
18
+ runPreScan(context: ProjectContext): Promise<void>;
19
+ runPostScan(result: AnalysisResult): Promise<AnalysisResult>;
20
+ }
@@ -0,0 +1,69 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ export class PluginManager {
4
+ plugins;
5
+ constructor(plugins) {
6
+ this.plugins = plugins;
7
+ }
8
+ static async load(rootDir, config) {
9
+ const specs = [
10
+ ...config.plugins.enabled.map((name) => `dep-brain-plugin-${name}`),
11
+ ...config.plugins.paths
12
+ ];
13
+ const plugins = [];
14
+ for (const spec of specs) {
15
+ const plugin = await loadPlugin(rootDir, spec);
16
+ if (plugin) {
17
+ plugins.push(plugin);
18
+ }
19
+ }
20
+ return new PluginManager(plugins);
21
+ }
22
+ async runPreScan(context) {
23
+ for (const plugin of this.plugins) {
24
+ await plugin.preScan?.(context);
25
+ }
26
+ }
27
+ async runPostScan(result) {
28
+ let current = result;
29
+ for (const plugin of this.plugins) {
30
+ const next = await plugin.postScan?.(current);
31
+ if (next) {
32
+ current = next;
33
+ }
34
+ const reportSection = await plugin.reportHook?.(current);
35
+ if (reportSection) {
36
+ current.extensions[plugin.name] = {
37
+ ...(asRecord(current.extensions[plugin.name]) ?? {}),
38
+ ...reportSection
39
+ };
40
+ }
41
+ }
42
+ return current;
43
+ }
44
+ }
45
+ async function loadPlugin(rootDir, spec) {
46
+ try {
47
+ const resolved = spec.startsWith(".") || path.isAbsolute(spec)
48
+ ? path.resolve(rootDir, spec)
49
+ : spec;
50
+ const moduleUrl = path.isAbsolute(resolved) ? pathToFileURL(resolved).href : resolved;
51
+ const mod = await import(moduleUrl);
52
+ const exported = mod.default ?? mod.plugin ?? mod;
53
+ const candidate = typeof exported === "function" ? new exported() : exported;
54
+ return isPlugin(candidate) ? candidate : null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ function isPlugin(value) {
61
+ return Boolean(value &&
62
+ typeof value === "object" &&
63
+ typeof value.name === "string");
64
+ }
65
+ function asRecord(value) {
66
+ return value && typeof value === "object" && !Array.isArray(value)
67
+ ? value
68
+ : null;
69
+ }
@@ -9,3 +9,10 @@ export interface ScoreInputs {
9
9
  riskWeight?: number;
10
10
  }
11
11
  export declare function calculateHealthScore(inputs: ScoreInputs): number;
12
+ export declare function calculateScoreDeductions(inputs: ScoreInputs): {
13
+ baseScore: number;
14
+ duplicates: number;
15
+ outdated: number;
16
+ unused: number;
17
+ risks: number;
18
+ };
@@ -1,12 +1,21 @@
1
1
  export function calculateHealthScore(inputs) {
2
+ const breakdown = calculateScoreDeductions(inputs);
3
+ return Math.max(0, breakdown.baseScore -
4
+ breakdown.duplicates -
5
+ breakdown.outdated -
6
+ breakdown.unused -
7
+ breakdown.risks);
8
+ }
9
+ export function calculateScoreDeductions(inputs) {
2
10
  const duplicateWeight = inputs.duplicateWeight ?? 5;
3
11
  const outdatedWeight = inputs.outdatedWeight ?? 3;
4
12
  const unusedWeight = inputs.unusedWeight ?? 4;
5
13
  const riskWeight = inputs.riskWeight ?? 10;
6
- const rawScore = 100 -
7
- inputs.duplicates * duplicateWeight -
8
- inputs.outdated * outdatedWeight -
9
- inputs.unused * unusedWeight -
10
- inputs.risks * riskWeight;
11
- return Math.max(0, rawScore);
14
+ return {
15
+ baseScore: 100,
16
+ duplicates: Math.min(35, inputs.duplicates * duplicateWeight),
17
+ outdated: Math.min(25, inputs.outdated * outdatedWeight),
18
+ unused: Math.min(20, inputs.unused * unusedWeight),
19
+ risks: Math.min(30, inputs.risks * riskWeight)
20
+ };
12
21
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { analyzeProject } from "./core/analyzer.js";
2
- export type { AnalysisOptions, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
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
+ export { PluginManager } from "./core/plugin-manager.js";
5
+ export type { DepBrainPlugin, ProjectContext } from "./core/plugin-manager.js";
4
6
  export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
5
7
  export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
6
8
  export type { WorkspacePackage } from "./utils/workspaces.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { analyzeProject } from "./core/analyzer.js";
2
2
  export { OUTPUT_VERSION } from "./core/analyzer.js";
3
+ export { PluginManager } from "./core/plugin-manager.js";
@@ -19,6 +19,21 @@ export interface DepBrainConfig {
19
19
  report: {
20
20
  maxSuggestions: number;
21
21
  };
22
+ plugins: {
23
+ enabled: string[];
24
+ paths: string[];
25
+ };
26
+ risk: {
27
+ transitiveBloatThreshold: number;
28
+ typosquattingDistanceThreshold: number;
29
+ };
30
+ dashboard: {
31
+ outputPath: string;
32
+ };
33
+ notifications: {
34
+ slackWebhookEnv: string;
35
+ discordWebhookEnv: string;
36
+ };
22
37
  scoring: {
23
38
  duplicateWeight: number;
24
39
  outdatedWeight: number;
@@ -33,6 +48,10 @@ export interface DepBrainConfigOverrides {
33
48
  ignore?: Partial<DepBrainConfig["ignore"]>;
34
49
  policy?: Partial<DepBrainConfig["policy"]>;
35
50
  report?: Partial<DepBrainConfig["report"]>;
51
+ plugins?: Partial<DepBrainConfig["plugins"]>;
52
+ risk?: Partial<DepBrainConfig["risk"]>;
53
+ dashboard?: Partial<DepBrainConfig["dashboard"]>;
54
+ notifications?: Partial<DepBrainConfig["notifications"]>;
36
55
  scoring?: Partial<DepBrainConfig["scoring"]>;
37
56
  scan?: Partial<DepBrainConfig["scan"]>;
38
57
  }
@@ -21,6 +21,21 @@ export const defaultConfig = {
21
21
  report: {
22
22
  maxSuggestions: 5
23
23
  },
24
+ plugins: {
25
+ enabled: [],
26
+ paths: []
27
+ },
28
+ risk: {
29
+ transitiveBloatThreshold: 50,
30
+ typosquattingDistanceThreshold: 2
31
+ },
32
+ dashboard: {
33
+ outputPath: "depbrain-dashboard.html"
34
+ },
35
+ notifications: {
36
+ slackWebhookEnv: "DEPBRAIN_SLACK_WEBHOOK_URL",
37
+ discordWebhookEnv: "DEPBRAIN_DISCORD_WEBHOOK_URL"
38
+ },
24
39
  scoring: {
25
40
  duplicateWeight: 5,
26
41
  outdatedWeight: 3,
@@ -63,6 +78,21 @@ function normalizeConfig(loaded) {
63
78
  report: {
64
79
  maxSuggestions: normalizeNumber(loaded.report?.maxSuggestions, defaultConfig.report.maxSuggestions)
65
80
  },
81
+ plugins: {
82
+ enabled: normalizeStringArray(loaded.plugins?.enabled, defaultConfig.plugins.enabled),
83
+ paths: normalizeStringArray(loaded.plugins?.paths, defaultConfig.plugins.paths)
84
+ },
85
+ risk: {
86
+ transitiveBloatThreshold: normalizeNumber(loaded.risk?.transitiveBloatThreshold, defaultConfig.risk.transitiveBloatThreshold),
87
+ typosquattingDistanceThreshold: normalizeNumber(loaded.risk?.typosquattingDistanceThreshold, defaultConfig.risk.typosquattingDistanceThreshold)
88
+ },
89
+ dashboard: {
90
+ outputPath: normalizeString(loaded.dashboard?.outputPath, defaultConfig.dashboard.outputPath)
91
+ },
92
+ notifications: {
93
+ slackWebhookEnv: normalizeString(loaded.notifications?.slackWebhookEnv, defaultConfig.notifications.slackWebhookEnv),
94
+ discordWebhookEnv: normalizeString(loaded.notifications?.discordWebhookEnv, defaultConfig.notifications.discordWebhookEnv)
95
+ },
66
96
  scoring: {
67
97
  duplicateWeight: normalizeNumber(loaded.scoring?.duplicateWeight, defaultConfig.scoring.duplicateWeight),
68
98
  outdatedWeight: normalizeNumber(loaded.scoring?.outdatedWeight, defaultConfig.scoring.outdatedWeight),
@@ -86,3 +116,6 @@ function normalizeBoolean(value, fallback) {
86
116
  function normalizeNumber(value, fallback) {
87
117
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
88
118
  }
119
+ function normalizeString(value, fallback) {
120
+ return typeof value === "string" && value.trim().length > 0 ? value : fallback;
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "CLI and library for explainable dependency intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",