dep-brain 0.9.0 → 1.0.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,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 1.0.0
6
+
7
+ - Stable v1 CLI and library release for explainable dependency intelligence.
8
+ - Added baseline mode with `--baseline <file>` to ignore existing dependency debt in CI.
9
+ - Added reusable GitHub Action metadata through `action.yml`.
10
+ - Added SARIF export support for code scanning workflows.
11
+ - Added stable JSON output fields for confidence, reason codes, explanations, recommendations, top issues, score breakdown, and workspace ownership summaries.
12
+ - Added bounded registry request parallelism for outdated and risk checks.
13
+ - Documented v1 readiness, release validation, and CI usage.
14
+
5
15
  ## 0.9.0
6
16
 
7
17
  - Workspace-aware analysis for npm workspaces.
package/README.md CHANGED
@@ -17,13 +17,15 @@
17
17
 
18
18
  ## What It Does
19
19
 
20
- - Detect duplicate dependencies from `package-lock.json`
20
+ - Detect duplicate dependencies from npm, pnpm, and yarn lockfiles
21
21
  - Detect likely unused dependencies from source imports and scripts
22
22
  - Detect outdated packages
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 human-readable or JSON format
26
+ - Output reports in console, JSON, Markdown, SARIF, and top-issues formats
27
+ - Gate CI with score and finding policies
28
+ - Compare new findings against a baseline report
27
29
 
28
30
  The long-term goal is not just to list problems, but to answer:
29
31
 
@@ -31,18 +33,24 @@ The long-term goal is not just to list problems, but to answer:
31
33
  - Can I remove it safely?
32
34
  - What should I fix first?
33
35
 
34
- ## Current MVP Features
36
+ ## v1 Features
35
37
 
36
38
  - Duplicate dependency detection with lockfile instance tracking
37
39
  - Unused dependency detection with runtime vs dev-tool heuristics
38
40
  - Outdated dependency reporting with `major`, `minor`, and `patch` classification
39
41
  - Risk analysis based on npm package metadata
42
+ - Confidence scores, reason codes, explanations, and recommendations for findings
40
43
  - Config loading from `depbrain.config.json`
41
44
  - Ignore rules for noisy dependencies and checks
42
45
  - CI-friendly policy evaluation with non-zero exit codes
43
46
  - Workspace-aware analysis for npm workspaces
44
47
  - Console reporting
45
48
  - JSON output via `--json`
49
+ - Markdown output via `--md`
50
+ - SARIF output via `--sarif`
51
+ - Ranked top issues via `--top`
52
+ - Baseline mode via `--baseline`
53
+ - Reusable GitHub Action via `action.yml`
46
54
  - Library entrypoint for programmatic use
47
55
 
48
56
  ## CLI Usage
@@ -61,6 +69,9 @@ npx dep-brain analyze --min-score 90 --fail-on-risks
61
69
  npx dep-brain analyze ./path-to-project --fail-on-unused --json
62
70
  npx dep-brain analyze --md > depbrain.md
63
71
  npx dep-brain analyze --json --out depbrain.json
72
+ npx dep-brain analyze --sarif --out depbrain.sarif
73
+ npx dep-brain analyze --baseline depbrain-baseline.json
74
+ npx dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-risks
64
75
  npx dep-brain report --from depbrain.json --md --out depbrain.md
65
76
 
66
77
  dep-brain config
@@ -138,6 +149,41 @@ dep-brain analyze --json --out depbrain.json
138
149
  dep-brain report --from depbrain.json --md --out depbrain.md
139
150
  ```
140
151
 
152
+ ## Baseline Mode
153
+
154
+ Baseline mode lets teams adopt `dep-brain` in existing repositories without failing CI for known dependency debt.
155
+
156
+ ```bash
157
+ dep-brain analyze --json --out depbrain-baseline.json
158
+ dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-risks
159
+ ```
160
+
161
+ 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.
162
+
163
+ ## GitHub Action
164
+
165
+ ```yaml
166
+ name: Dependency Brain
167
+
168
+ on:
169
+ pull_request:
170
+
171
+ jobs:
172
+ dep-brain:
173
+ runs-on: ubuntu-latest
174
+ steps:
175
+ - uses: actions/checkout@v4
176
+ - uses: actions/setup-node@v4
177
+ with:
178
+ node-version: 22
179
+ - uses: prakashu51/dep-brain@v1
180
+ with:
181
+ format: sarif
182
+ out: depbrain.sarif
183
+ min-score: 85
184
+ fail-on-risks: "true"
185
+ ```
186
+
141
187
  ## Config File
142
188
 
143
189
  Create a `depbrain.config.json` file in the project root:
@@ -208,6 +254,7 @@ Examples:
208
254
  dep-brain analyze --fail-on-unused
209
255
  dep-brain analyze --min-score 85 --fail-on-risks
210
256
  dep-brain analyze --config depbrain.config.json
257
+ dep-brain analyze --baseline depbrain-baseline.json --fail-on-unused
211
258
  ```
212
259
 
213
260
  ## Config Debugging
@@ -254,7 +301,7 @@ src/
254
301
 
255
302
  ## Product Direction
256
303
 
257
- `dep-brain` is currently in the `v0.5.x` foundation stage. The next roadmap is:
304
+ `dep-brain` is in its `v1.0.0` production-ready CLI stage. The roadmap delivered through v1:
258
305
 
259
306
  - `v0.6`: explainability and confidence scoring
260
307
  - `v0.7`: safe removal guidance and actionable recommendations
package/action.yml ADDED
@@ -0,0 +1,116 @@
1
+ name: Dependency Brain
2
+ description: Run dep-brain dependency intelligence checks in GitHub Actions.
3
+ author: dep-brain
4
+
5
+ inputs:
6
+ path:
7
+ description: Project path to analyze.
8
+ required: false
9
+ default: "."
10
+ format:
11
+ description: Output format. Use console, json, md, sarif, or top.
12
+ required: false
13
+ default: "console"
14
+ out:
15
+ description: Optional path to write the report.
16
+ required: false
17
+ default: ""
18
+ config:
19
+ description: Optional path to depbrain.config.json.
20
+ required: false
21
+ default: ""
22
+ baseline:
23
+ description: Optional baseline JSON report used to ignore existing findings.
24
+ required: false
25
+ default: ""
26
+ min-score:
27
+ description: Minimum project health score required to pass.
28
+ required: false
29
+ default: ""
30
+ fail-on-unused:
31
+ description: Fail when unused dependencies are found.
32
+ required: false
33
+ default: "false"
34
+ fail-on-outdated:
35
+ description: Fail when outdated dependencies are found.
36
+ required: false
37
+ default: "false"
38
+ fail-on-duplicates:
39
+ description: Fail when duplicate dependencies are found.
40
+ required: false
41
+ default: "false"
42
+ fail-on-risks:
43
+ description: Fail when risky dependencies are found.
44
+ required: false
45
+ default: "false"
46
+
47
+ runs:
48
+ using: composite
49
+ steps:
50
+ - name: Run dep-brain
51
+ shell: bash
52
+ env:
53
+ INPUT_PATH: ${{ inputs.path }}
54
+ INPUT_FORMAT: ${{ inputs.format }}
55
+ INPUT_OUT: ${{ inputs.out }}
56
+ INPUT_CONFIG: ${{ inputs.config }}
57
+ INPUT_BASELINE: ${{ inputs.baseline }}
58
+ INPUT_MIN_SCORE: ${{ inputs.min-score }}
59
+ INPUT_FAIL_ON_UNUSED: ${{ inputs.fail-on-unused }}
60
+ INPUT_FAIL_ON_OUTDATED: ${{ inputs.fail-on-outdated }}
61
+ INPUT_FAIL_ON_DUPLICATES: ${{ inputs.fail-on-duplicates }}
62
+ INPUT_FAIL_ON_RISKS: ${{ inputs.fail-on-risks }}
63
+ run: |
64
+ set -euo pipefail
65
+
66
+ args=("analyze" "$INPUT_PATH")
67
+
68
+ case "$INPUT_FORMAT" in
69
+ json) args+=("--json") ;;
70
+ md) args+=("--md") ;;
71
+ sarif) args+=("--sarif") ;;
72
+ top) args+=("--top") ;;
73
+ console) ;;
74
+ *)
75
+ echo "Unsupported format: $INPUT_FORMAT" >&2
76
+ exit 1
77
+ ;;
78
+ esac
79
+
80
+ if [ -n "$INPUT_OUT" ]; then
81
+ args+=("--out" "$INPUT_OUT")
82
+ fi
83
+
84
+ if [ -n "$INPUT_CONFIG" ]; then
85
+ args+=("--config" "$INPUT_CONFIG")
86
+ fi
87
+
88
+ if [ -n "$INPUT_BASELINE" ]; then
89
+ args+=("--baseline" "$INPUT_BASELINE")
90
+ fi
91
+
92
+ if [ -n "$INPUT_MIN_SCORE" ]; then
93
+ args+=("--min-score" "$INPUT_MIN_SCORE")
94
+ fi
95
+
96
+ if [ "$INPUT_FAIL_ON_UNUSED" = "true" ]; then
97
+ args+=("--fail-on-unused")
98
+ fi
99
+
100
+ if [ "$INPUT_FAIL_ON_OUTDATED" = "true" ]; then
101
+ args+=("--fail-on-outdated")
102
+ fi
103
+
104
+ if [ "$INPUT_FAIL_ON_DUPLICATES" = "true" ]; then
105
+ args+=("--fail-on-duplicates")
106
+ fi
107
+
108
+ if [ "$INPUT_FAIL_ON_RISKS" = "true" ]; then
109
+ args+=("--fail-on-risks")
110
+ fi
111
+
112
+ node "$GITHUB_ACTION_PATH/dist/cli.js" "${args[@]}"
113
+
114
+ branding:
115
+ icon: activity
116
+ color: blue
@@ -77,7 +77,6 @@
77
77
  "reasons": { "type": "array", "items": { "type": "string" } }
78
78
  }
79
79
  },
80
- "ownershipSummary": { "$ref": "#/properties/ownershipSummary" },
81
80
  "duplicates": {
82
81
  "type": "array",
83
82
  "items": {
@@ -5,7 +5,7 @@ export async function findOutdatedDependencies(graph, options = {}) {
5
5
  ...graph.dependencies,
6
6
  ...graph.devDependencies
7
7
  };
8
- const results = await Promise.all(Object.entries(combined).map(async ([name, current]) => {
8
+ const results = await mapWithConcurrency(Object.entries(combined), 8, async ([name, current]) => {
9
9
  const normalized = normalizeVersion(current);
10
10
  const latest = await resolveLatestVersion(name);
11
11
  if (!latest || latest === normalized) {
@@ -28,11 +28,25 @@ export async function findOutdatedDependencies(graph, options = {}) {
28
28
  ],
29
29
  recommendation: buildOutdatedRecommendation(updateType)
30
30
  };
31
- }));
31
+ });
32
32
  return results
33
33
  .filter((item) => item !== null)
34
34
  .sort((left, right) => left.name.localeCompare(right.name));
35
35
  }
36
+ async function mapWithConcurrency(items, limit, mapper) {
37
+ const results = new Array(items.length);
38
+ let nextIndex = 0;
39
+ async function worker() {
40
+ while (nextIndex < items.length) {
41
+ const currentIndex = nextIndex;
42
+ nextIndex += 1;
43
+ results[currentIndex] = await mapper(items[currentIndex]);
44
+ }
45
+ }
46
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
47
+ await Promise.all(workers);
48
+ return results;
49
+ }
36
50
  function buildOutdatedRecommendation(updateType) {
37
51
  return {
38
52
  action: "upgrade",
@@ -7,7 +7,7 @@ export async function findRiskDependencies(graph, options = {}) {
7
7
  ...graph.dependencies,
8
8
  ...graph.devDependencies
9
9
  });
10
- const results = await Promise.all(names.map(async (name) => {
10
+ const results = await mapWithConcurrency(names, 8, async (name) => {
11
11
  const metadata = await resolvePackageMetadata(name);
12
12
  if (!metadata) {
13
13
  return null;
@@ -31,11 +31,25 @@ export async function findRiskDependencies(graph, options = {}) {
31
31
  riskFactors: assessment.riskFactors,
32
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
+ async function mapWithConcurrency(items, limit, mapper) {
40
+ const results = new Array(items.length);
41
+ let nextIndex = 0;
42
+ async function worker() {
43
+ while (nextIndex < items.length) {
44
+ const currentIndex = nextIndex;
45
+ nextIndex += 1;
46
+ results[currentIndex] = await mapper(items[currentIndex]);
47
+ }
48
+ }
49
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
50
+ await Promise.all(workers);
51
+ return results;
52
+ }
39
53
  export async function runRiskCheck(graph) {
40
54
  const risks = await findRiskDependencies(graph);
41
55
  return {
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { analyzeProject } from "./core/analyzer.js";
3
3
  import { renderConsoleReport } from "./reporters/console.js";
4
4
  import { renderJsonReport } from "./reporters/json.js";
5
5
  import { renderMarkdownReport } from "./reporters/markdown.js";
6
+ import { renderSarifReport } from "./reporters/sarif.js";
6
7
  import { promises as fs } from "node:fs";
7
8
  import path from "node:path";
8
9
  async function main() {
@@ -55,7 +56,9 @@ async function main() {
55
56
  ? renderTopIssuesReport(reportData)
56
57
  : flags.has("--json")
57
58
  ? JSON.stringify(reportData, null, 2)
58
- : renderMarkdownReport(reportData);
59
+ : flags.has("--sarif")
60
+ ? renderSarifReport(reportData)
61
+ : renderMarkdownReport(reportData);
59
62
  await writeOutput(output, optionValues.get("--out"));
60
63
  return;
61
64
  }
@@ -100,15 +103,20 @@ async function main() {
100
103
  }
101
104
  try {
102
105
  const cliConfig = buildCliConfig(flags, optionValues);
106
+ const baseline = await loadBaseline(optionValues.get("--baseline"));
103
107
  const result = await analyzeProject({
104
108
  rootDir: targetPath,
105
109
  configPath: optionValues.get("--config"),
106
- config: cliConfig
110
+ config: cliConfig,
111
+ baseline
107
112
  });
108
113
  let output;
109
114
  if (flags.has("--json")) {
110
115
  output = renderJsonReport(result);
111
116
  }
117
+ else if (flags.has("--sarif")) {
118
+ output = renderSarifReport(result);
119
+ }
112
120
  else if (flags.has("--top")) {
113
121
  output = renderTopIssuesReport(result);
114
122
  }
@@ -178,8 +186,8 @@ function printHelp() {
178
186
  console.log("Dependency Brain");
179
187
  console.log("");
180
188
  console.log("Usage:");
181
- console.log(" dep-brain analyze [path] [--json] [--md] [--top] [--out path] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
182
- console.log(" dep-brain report --from <file> [--md] [--json] [--top] [--out path]");
189
+ console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
190
+ console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--out path]");
183
191
  console.log(" dep-brain config [path] [--config path]");
184
192
  console.log(" dep-brain help");
185
193
  console.log(" dep-brain --version");
@@ -187,8 +195,10 @@ function printHelp() {
187
195
  console.log("Options:");
188
196
  console.log(" --json Output JSON for analysis");
189
197
  console.log(" --md Output Markdown report");
198
+ console.log(" --sarif Output SARIF format for Code Scanning");
190
199
  console.log(" --top Output the ranked top issues only");
191
200
  console.log(" --config <path> Path to depbrain.config.json");
201
+ console.log(" --baseline <path> Ignore findings already present in a baseline JSON report");
192
202
  console.log(" --from <file> Read analysis JSON from file");
193
203
  console.log(" --out <path> Write output to a file");
194
204
  console.log(" --min-score <n> Minimum score required to pass");
@@ -210,6 +220,20 @@ async function loadPackageVersion() {
210
220
  return null;
211
221
  }
212
222
  }
223
+ async function loadBaseline(baselinePath) {
224
+ if (!baselinePath) {
225
+ return undefined;
226
+ }
227
+ const resolved = resolveUserPath(baselinePath);
228
+ const raw = await fs.readFile(resolved, "utf8");
229
+ const parsed = JSON.parse(raw);
230
+ return {
231
+ duplicates: Array.isArray(parsed.duplicates) ? parsed.duplicates : [],
232
+ unused: Array.isArray(parsed.unused) ? parsed.unused : [],
233
+ outdated: Array.isArray(parsed.outdated) ? parsed.outdated : [],
234
+ risks: Array.isArray(parsed.risks) ? parsed.risks : []
235
+ };
236
+ }
213
237
  async function writeOutput(output, outPath) {
214
238
  if (outPath) {
215
239
  const resolved = resolveUserPath(outPath);
@@ -3,6 +3,13 @@ export interface AnalysisOptions {
3
3
  rootDir?: string;
4
4
  configPath?: string;
5
5
  config?: DepBrainConfigOverrides;
6
+ baseline?: DepBrainBaseline;
7
+ }
8
+ export interface DepBrainBaseline {
9
+ duplicates?: Array<Partial<Pick<DuplicateDependency, "name">>>;
10
+ unused?: Array<Partial<Pick<UnusedDependency, "name" | "section" | "package">>>;
11
+ outdated?: Array<Partial<Pick<OutdatedDependency, "name" | "current" | "latest" | "package">>>;
12
+ risks?: Array<Partial<Pick<RiskDependency, "name" | "package">>>;
6
13
  }
7
14
  export interface DuplicateInstance {
8
15
  path: string;
@@ -15,7 +15,9 @@ export async function analyzeProject(options = {}) {
15
15
  const config = mergeConfig(loadedConfig, options.config);
16
16
  const workspaces = await findWorkspacePackages(rootDir);
17
17
  if (workspaces.length === 0) {
18
- return analyzeSingleProject(rootDir, config);
18
+ return analyzeSingleProject(rootDir, config, {
19
+ baseline: options.baseline
20
+ });
19
21
  }
20
22
  const rootGraph = await buildDependencyGraph(rootDir);
21
23
  const duplicateCheck = await runDuplicateCheck(rootGraph);
@@ -33,11 +35,21 @@ export async function analyzeProject(options = {}) {
33
35
  graph: await buildDependencyGraph(workspace.rootDir)
34
36
  })));
35
37
  const attributedDuplicates = addWorkspaceAttribution(duplicates, workspaceGraphs);
36
- const unused = packages.flatMap((pkg) => pkg.unused.map((item) => ({ ...item, package: pkg.name })));
37
- const outdated = packages.flatMap((pkg) => pkg.outdated.map((item) => ({ ...item, package: pkg.name })));
38
- const risks = packages.flatMap((pkg) => pkg.risks.map((item) => ({ ...item, package: pkg.name })));
38
+ const rawUnused = packages.flatMap((pkg) => pkg.unused.map((item) => ({ ...item, package: pkg.name })));
39
+ const rawOutdated = packages.flatMap((pkg) => pkg.outdated.map((item) => ({ ...item, package: pkg.name })));
40
+ const rawRisks = packages.flatMap((pkg) => pkg.risks.map((item) => ({ ...item, package: pkg.name })));
41
+ const baselineFiltered = filterBaselineFindings({
42
+ duplicates: attributedDuplicates,
43
+ unused: rawUnused,
44
+ outdated: rawOutdated,
45
+ risks: rawRisks
46
+ }, options.baseline);
47
+ const activeDuplicates = baselineFiltered.duplicates;
48
+ const unused = baselineFiltered.unused;
49
+ const outdated = baselineFiltered.outdated;
50
+ const risks = baselineFiltered.risks;
39
51
  const score = calculateHealthScore({
40
- duplicates: duplicates.length,
52
+ duplicates: activeDuplicates.length,
41
53
  unused: unused.length,
42
54
  outdated: outdated.length,
43
55
  risks: risks.length,
@@ -47,17 +59,19 @@ export async function analyzeProject(options = {}) {
47
59
  riskWeight: config.scoring.riskWeight
48
60
  });
49
61
  const scoreBreakdown = buildScoreBreakdown({
50
- duplicates: duplicates.length,
62
+ duplicates: activeDuplicates.length,
51
63
  unused: unused.length,
52
64
  outdated: outdated.length,
53
65
  risks: risks.length
54
66
  }, config);
55
67
  const suggestions = [
56
- ...packages.flatMap((pkg) => pkg.suggestions.map((suggestion) => `[${pkg.name}] ${suggestion}`))
68
+ ...unused.map((item) => `Remove ${item.name} from ${item.section}`),
69
+ ...activeDuplicates.map((item) => `Consider consolidating ${item.name} to one version`),
70
+ ...outdated.map((item) => `Review ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})`)
57
71
  ].slice(0, config.report.maxSuggestions);
58
72
  const policy = evaluatePolicy({
59
73
  score,
60
- duplicates: attributedDuplicates.length,
74
+ duplicates: activeDuplicates.length,
61
75
  unused: unused.length,
62
76
  outdated: outdated.length,
63
77
  risks: risks.length
@@ -69,18 +83,18 @@ export async function analyzeProject(options = {}) {
69
83
  scoreBreakdown,
70
84
  policy,
71
85
  ownershipSummary: buildOwnershipSummary({
72
- duplicates: attributedDuplicates,
86
+ duplicates: activeDuplicates,
73
87
  unused,
74
88
  outdated,
75
89
  risks
76
90
  }),
77
- duplicates: attributedDuplicates,
91
+ duplicates: activeDuplicates,
78
92
  unused,
79
93
  outdated,
80
94
  risks,
81
95
  suggestions,
82
96
  topIssues: buildTopIssues({
83
- duplicates: attributedDuplicates,
97
+ duplicates: activeDuplicates,
84
98
  unused,
85
99
  outdated,
86
100
  risks
@@ -155,43 +169,49 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
155
169
  const unused = mapUnusedIssues(issueGroups.unused);
156
170
  const outdated = mapOutdatedIssues(issueGroups.outdated);
157
171
  const risks = mapRiskIssues(issueGroups.risks);
172
+ const scopedUnused = options.packageName && options.packageName.trim().length > 0
173
+ ? unused.map((item) => ({ ...item, package: options.packageName }))
174
+ : unused;
175
+ const scopedOutdated = options.packageName && options.packageName.trim().length > 0
176
+ ? outdated.map((item) => ({ ...item, package: options.packageName }))
177
+ : outdated;
178
+ const scopedRisks = options.packageName && options.packageName.trim().length > 0
179
+ ? risks.map((item) => ({ ...item, package: options.packageName }))
180
+ : risks;
181
+ const baselineFiltered = filterBaselineFindings({
182
+ duplicates,
183
+ unused: scopedUnused,
184
+ outdated: scopedOutdated,
185
+ risks: scopedRisks
186
+ }, options.baseline);
158
187
  const score = calculateHealthScore({
159
- duplicates: duplicates.length,
160
- unused: unused.length,
161
- outdated: outdated.length,
162
- risks: risks.length,
188
+ duplicates: baselineFiltered.duplicates.length,
189
+ unused: baselineFiltered.unused.length,
190
+ outdated: baselineFiltered.outdated.length,
191
+ risks: baselineFiltered.risks.length,
163
192
  duplicateWeight: config.scoring.duplicateWeight,
164
193
  outdatedWeight: config.scoring.outdatedWeight,
165
194
  unusedWeight: config.scoring.unusedWeight,
166
195
  riskWeight: config.scoring.riskWeight
167
196
  });
168
197
  const scoreBreakdown = buildScoreBreakdown({
169
- duplicates: duplicates.length,
170
- unused: unused.length,
171
- outdated: outdated.length,
172
- risks: risks.length
198
+ duplicates: baselineFiltered.duplicates.length,
199
+ unused: baselineFiltered.unused.length,
200
+ outdated: baselineFiltered.outdated.length,
201
+ risks: baselineFiltered.risks.length
173
202
  }, config);
174
203
  const suggestions = [
175
- ...unused.map((item) => `Remove ${item.name} from ${item.section}`),
176
- ...duplicates.map((item) => `Consider consolidating ${item.name} to one version`),
177
- ...outdated.map((item) => `Review ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})`)
204
+ ...baselineFiltered.unused.map((item) => `Remove ${item.name} from ${item.section}`),
205
+ ...baselineFiltered.duplicates.map((item) => `Consider consolidating ${item.name} to one version`),
206
+ ...baselineFiltered.outdated.map((item) => `Review ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})`)
178
207
  ].slice(0, config.report.maxSuggestions);
179
208
  const policy = evaluatePolicy({
180
209
  score,
181
- duplicates: duplicates.length,
182
- unused: unused.length,
183
- outdated: outdated.length,
184
- risks: risks.length
210
+ duplicates: baselineFiltered.duplicates.length,
211
+ unused: baselineFiltered.unused.length,
212
+ outdated: baselineFiltered.outdated.length,
213
+ risks: baselineFiltered.risks.length
185
214
  }, config);
186
- const scopedUnused = options.packageName && options.packageName.trim().length > 0
187
- ? unused.map((item) => ({ ...item, package: options.packageName }))
188
- : unused;
189
- const scopedOutdated = options.packageName && options.packageName.trim().length > 0
190
- ? outdated.map((item) => ({ ...item, package: options.packageName }))
191
- : outdated;
192
- const scopedRisks = options.packageName && options.packageName.trim().length > 0
193
- ? risks.map((item) => ({ ...item, package: options.packageName }))
194
- : risks;
195
215
  return {
196
216
  outputVersion: OUTPUT_VERSION,
197
217
  rootDir,
@@ -199,21 +219,21 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
199
219
  scoreBreakdown,
200
220
  policy,
201
221
  ownershipSummary: buildOwnershipSummary({
202
- duplicates,
203
- unused: scopedUnused,
204
- outdated: scopedOutdated,
205
- risks: scopedRisks
222
+ duplicates: baselineFiltered.duplicates,
223
+ unused: baselineFiltered.unused,
224
+ outdated: baselineFiltered.outdated,
225
+ risks: baselineFiltered.risks
206
226
  }),
207
- duplicates,
208
- unused: scopedUnused,
209
- outdated: scopedOutdated,
210
- risks: scopedRisks,
227
+ duplicates: baselineFiltered.duplicates,
228
+ unused: baselineFiltered.unused,
229
+ outdated: baselineFiltered.outdated,
230
+ risks: baselineFiltered.risks,
211
231
  suggestions,
212
232
  topIssues: buildTopIssues({
213
- duplicates,
214
- unused: scopedUnused,
215
- outdated: scopedOutdated,
216
- risks: scopedRisks
233
+ duplicates: baselineFiltered.duplicates,
234
+ unused: baselineFiltered.unused,
235
+ outdated: baselineFiltered.outdated,
236
+ risks: baselineFiltered.risks
217
237
  }),
218
238
  config
219
239
  };
@@ -497,6 +517,26 @@ function buildOwnershipSummary(input) {
497
517
  risks: input.risks.length
498
518
  };
499
519
  }
520
+ function filterBaselineFindings(findings, baseline) {
521
+ if (!baseline) {
522
+ return findings;
523
+ }
524
+ return {
525
+ duplicates: findings.duplicates.filter((item) => !(baseline.duplicates ?? []).some((entry) => entry.name === item.name)),
526
+ unused: findings.unused.filter((item) => !(baseline.unused ?? []).some((entry) => entry.name === item.name &&
527
+ optionalMatches(entry.section, item.section) &&
528
+ optionalMatches(entry.package, item.package))),
529
+ outdated: findings.outdated.filter((item) => !(baseline.outdated ?? []).some((entry) => entry.name === item.name &&
530
+ optionalMatches(entry.current, item.current) &&
531
+ optionalMatches(entry.latest, item.latest) &&
532
+ optionalMatches(entry.package, item.package))),
533
+ risks: findings.risks.filter((item) => !(baseline.risks ?? []).some((entry) => entry.name === item.name &&
534
+ optionalMatches(entry.package, item.package)))
535
+ };
536
+ }
537
+ function optionalMatches(expected, actual) {
538
+ return expected === undefined || expected === actual;
539
+ }
500
540
  function normalizeConfidence(value) {
501
541
  if (typeof value !== "number" || Number.isNaN(value)) {
502
542
  return 0.5;
@@ -1,8 +1,11 @@
1
1
  import path from "node:path";
2
+ import { promises as fs } from "node:fs";
2
3
  import { readJsonFile } from "../utils/file-parser.js";
3
4
  export async function buildDependencyGraph(rootDir) {
4
5
  const packageJsonPath = path.join(rootDir, "package.json");
5
6
  const lockfilePath = path.join(rootDir, "package-lock.json");
7
+ const pnpmLockfilePath = path.join(rootDir, "pnpm-lock.yaml");
8
+ const yarnLockfilePath = path.join(rootDir, "yarn.lock");
6
9
  const packageJson = await readJsonFile(packageJsonPath);
7
10
  const lockPackages = new Map();
8
11
  try {
@@ -29,13 +32,15 @@ export async function buildDependencyGraph(rootDir) {
29
32
  }
30
33
  }
31
34
  catch {
35
+ const fallbackLockfile = await readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath);
32
36
  return {
33
37
  rootDir,
34
38
  packageJsonPath,
39
+ lockfilePath: fallbackLockfile.lockfilePath,
35
40
  dependencies: packageJson.dependencies ?? {},
36
41
  devDependencies: packageJson.devDependencies ?? {},
37
42
  scripts: packageJson.scripts ?? {},
38
- lockPackages: {}
43
+ lockPackages: fallbackLockfile.lockPackages
39
44
  };
40
45
  }
41
46
  return {
@@ -51,6 +56,30 @@ export async function buildDependencyGraph(rootDir) {
51
56
  ]))
52
57
  };
53
58
  }
59
+ async function readAlternativeLockfile(pnpmLockfilePath, yarnLockfilePath) {
60
+ try {
61
+ const content = await fs.readFile(pnpmLockfilePath, "utf8");
62
+ return {
63
+ lockfilePath: pnpmLockfilePath,
64
+ lockPackages: parsePnpmLockfile(content)
65
+ };
66
+ }
67
+ catch {
68
+ // Try yarn.lock below.
69
+ }
70
+ try {
71
+ const content = await fs.readFile(yarnLockfilePath, "utf8");
72
+ return {
73
+ lockfilePath: yarnLockfilePath,
74
+ lockPackages: parseYarnLockfile(content)
75
+ };
76
+ }
77
+ catch {
78
+ return {
79
+ lockPackages: {}
80
+ };
81
+ }
82
+ }
54
83
  function extractPackageName(packagePath) {
55
84
  if (!packagePath) {
56
85
  return null;
@@ -61,3 +90,66 @@ function extractPackageName(packagePath) {
61
90
  }
62
91
  return match[1];
63
92
  }
93
+ function parsePnpmLockfile(content) {
94
+ const lockPackages = new Map();
95
+ for (const line of content.split(/\r?\n/)) {
96
+ const match = line.match(/^\s{2}(?:'|")?\/((?:@[^/]+\/)?[^/@'"]+)@([^('":]+)[^:]*:(?:'|")?\s*$/);
97
+ if (!match) {
98
+ continue;
99
+ }
100
+ addLockPackage(lockPackages, match[1], `pnpm-lock:${match[0].trim()}`, match[2]);
101
+ }
102
+ return toLockPackageRecord(lockPackages);
103
+ }
104
+ function parseYarnLockfile(content) {
105
+ const lockPackages = new Map();
106
+ let currentNames = [];
107
+ for (const line of content.split(/\r?\n/)) {
108
+ if (line.trim().length === 0 || line.startsWith("#")) {
109
+ continue;
110
+ }
111
+ if (!line.startsWith(" ") && line.endsWith(":")) {
112
+ currentNames = extractYarnEntryNames(line.slice(0, -1));
113
+ continue;
114
+ }
115
+ const versionMatch = line.match(/^\s+version\s+"?([^"\s]+)"?\s*$/);
116
+ if (!versionMatch) {
117
+ continue;
118
+ }
119
+ for (const name of currentNames) {
120
+ addLockPackage(lockPackages, name, `yarn-lock:${name}@${versionMatch[1]}`, versionMatch[1]);
121
+ }
122
+ }
123
+ return toLockPackageRecord(lockPackages);
124
+ }
125
+ function extractYarnEntryNames(entry) {
126
+ const names = new Set();
127
+ const unquoted = entry.replace(/^["']|["']$/g, "");
128
+ for (const selector of unquoted.split(/,\s*/)) {
129
+ const normalized = selector.replace(/^["']|["']$/g, "");
130
+ const withoutProtocol = normalized.replace(/@npm:/, "@");
131
+ if (withoutProtocol.startsWith("@")) {
132
+ const scoped = withoutProtocol.match(/^(@[^/]+\/[^@]+)/);
133
+ if (scoped) {
134
+ names.add(scoped[1]);
135
+ }
136
+ continue;
137
+ }
138
+ const unscoped = withoutProtocol.match(/^([^@]+)/);
139
+ if (unscoped?.[1]) {
140
+ names.add(unscoped[1]);
141
+ }
142
+ }
143
+ return Array.from(names);
144
+ }
145
+ function addLockPackage(lockPackages, name, packagePath, version) {
146
+ const instances = lockPackages.get(name) ?? new Map();
147
+ instances.set(packagePath, { path: packagePath, version });
148
+ lockPackages.set(name, instances);
149
+ }
150
+ function toLockPackageRecord(lockPackages) {
151
+ return Object.fromEntries(Array.from(lockPackages.entries()).map(([name, instances]) => [
152
+ name,
153
+ Array.from(instances.values()).sort((left, right) => left.path.localeCompare(right.path))
154
+ ]));
155
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { analyzeProject } from "./core/analyzer.js";
2
- export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } 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";
3
3
  export { OUTPUT_VERSION } from "./core/analyzer.js";
4
4
  export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
5
5
  export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
@@ -0,0 +1,2 @@
1
+ import type { AnalysisResult } from "../core/analyzer.js";
2
+ export declare function renderSarifReport(result: AnalysisResult): string;
@@ -0,0 +1,126 @@
1
+ export function renderSarifReport(result) {
2
+ const rules = [
3
+ {
4
+ id: "dep-brain-unused",
5
+ shortDescription: { text: "Unused Dependency" },
6
+ fullDescription: { text: "A dependency was detected but does not appear to be used in the source code." },
7
+ helpUri: "https://github.com/prakashu51/dep-brain"
8
+ },
9
+ {
10
+ id: "dep-brain-duplicate",
11
+ shortDescription: { text: "Duplicate Dependency" },
12
+ fullDescription: { text: "Multiple versions of the same dependency exist in the lockfile." },
13
+ helpUri: "https://github.com/prakashu51/dep-brain"
14
+ },
15
+ {
16
+ id: "dep-brain-outdated",
17
+ shortDescription: { text: "Outdated Dependency" },
18
+ fullDescription: { text: "A newer version of the dependency is available." },
19
+ helpUri: "https://github.com/prakashu51/dep-brain"
20
+ },
21
+ {
22
+ id: "dep-brain-risk",
23
+ shortDescription: { text: "Risky Dependency" },
24
+ fullDescription: { text: "A dependency exhibits supply-chain risk signals." },
25
+ helpUri: "https://github.com/prakashu51/dep-brain"
26
+ }
27
+ ];
28
+ const results = [];
29
+ const mapLevel = (priority) => {
30
+ switch (priority) {
31
+ case "high": return "error";
32
+ case "medium": return "warning";
33
+ case "low": return "note";
34
+ default: return "none";
35
+ }
36
+ };
37
+ for (const item of result.unused) {
38
+ results.push({
39
+ ruleId: "dep-brain-unused",
40
+ level: mapLevel(item.recommendation.priority),
41
+ message: {
42
+ text: `[Unused] ${item.name}\n\n${item.recommendation.summary}\nReasons:\n- ${item.recommendation.reasons.join('\n- ')}`
43
+ },
44
+ locations: [
45
+ {
46
+ physicalLocation: {
47
+ artifactLocation: {
48
+ uri: item.package ? `packages/${item.package}/package.json` : "package.json"
49
+ }
50
+ }
51
+ }
52
+ ]
53
+ });
54
+ }
55
+ for (const item of result.duplicates) {
56
+ results.push({
57
+ ruleId: "dep-brain-duplicate",
58
+ level: mapLevel(item.recommendation.priority),
59
+ message: {
60
+ text: `[Duplicate] ${item.name} (${item.versions.join(", ")})\n\n${item.recommendation.summary}`
61
+ },
62
+ locations: [
63
+ {
64
+ physicalLocation: {
65
+ artifactLocation: {
66
+ uri: "package-lock.json"
67
+ }
68
+ }
69
+ }
70
+ ]
71
+ });
72
+ }
73
+ for (const item of result.outdated) {
74
+ results.push({
75
+ ruleId: "dep-brain-outdated",
76
+ level: mapLevel(item.recommendation.priority),
77
+ message: {
78
+ text: `[Outdated] ${item.name}: ${item.current} -> ${item.latest} (${item.updateType})\n\n${item.recommendation.summary}`
79
+ },
80
+ locations: [
81
+ {
82
+ physicalLocation: {
83
+ artifactLocation: {
84
+ uri: item.package ? `packages/${item.package}/package.json` : "package.json"
85
+ }
86
+ }
87
+ }
88
+ ]
89
+ });
90
+ }
91
+ for (const item of result.risks) {
92
+ results.push({
93
+ ruleId: "dep-brain-risk",
94
+ level: mapLevel(item.recommendation.priority),
95
+ message: {
96
+ text: `[Risk] ${item.name} (Trust: ${item.trustScore})\n\n${item.recommendation.summary}\nReasons:\n- ${item.recommendation.reasons.join('\n- ')}`
97
+ },
98
+ locations: [
99
+ {
100
+ physicalLocation: {
101
+ artifactLocation: {
102
+ uri: item.package ? `packages/${item.package}/package.json` : "package.json"
103
+ }
104
+ }
105
+ }
106
+ ]
107
+ });
108
+ }
109
+ const sarif = {
110
+ version: "2.1.0",
111
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
112
+ runs: [
113
+ {
114
+ tool: {
115
+ driver: {
116
+ name: "Dependency Brain",
117
+ informationUri: "https://github.com/prakashu51/dep-brain",
118
+ rules
119
+ }
120
+ },
121
+ results
122
+ }
123
+ ]
124
+ };
125
+ return JSON.stringify(sarif, null, 2);
126
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "0.9.0",
4
- "description": "CLI and library for dependency health analysis",
3
+ "version": "1.0.0",
4
+ "description": "CLI and library for explainable dependency intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -13,6 +13,7 @@
13
13
  "README.md",
14
14
  "LICENSE",
15
15
  "CHANGELOG.md",
16
+ "action.yml",
16
17
  "depbrain.config.json",
17
18
  "depbrain.config.schema.json",
18
19
  "depbrain.output.schema.json"