dep-brain 0.8.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,7 +2,17 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## Unreleased
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
+
15
+ ## 0.9.0
6
16
 
7
17
  - Workspace-aware analysis for npm workspaces.
8
18
  - Config loading and CI policy controls.
@@ -11,3 +21,5 @@ All notable changes to this project will be documented in this file.
11
21
  - Ranked top-issues summary output with `--top`.
12
22
  - Supply-chain trust scoring for risk findings.
13
23
  - Structured risk factors in JSON output.
24
+ - Monorepo ownership summaries for workspace packages.
25
+ - Workspace-level duplicate attribution and root-cause tracing.
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
@@ -75,6 +86,12 @@ dep-brain --version
75
86
 
76
87
  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.
77
88
 
89
+ Workspace analysis now includes:
90
+
91
+ - per-workspace ownership summaries
92
+ - root-level duplicate attribution back to contributing workspaces
93
+ - top issues that stay tagged to the workspace that should act
94
+
78
95
  ## Example Output
79
96
 
80
97
  ```text
@@ -132,6 +149,41 @@ dep-brain analyze --json --out depbrain.json
132
149
  dep-brain report --from depbrain.json --md --out depbrain.md
133
150
  ```
134
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
+
135
187
  ## Config File
136
188
 
137
189
  Create a `depbrain.config.json` file in the project root:
@@ -202,6 +254,7 @@ Examples:
202
254
  dep-brain analyze --fail-on-unused
203
255
  dep-brain analyze --min-score 85 --fail-on-risks
204
256
  dep-brain analyze --config depbrain.config.json
257
+ dep-brain analyze --baseline depbrain-baseline.json --fail-on-unused
205
258
  ```
206
259
 
207
260
  ## Config Debugging
@@ -248,7 +301,7 @@ src/
248
301
 
249
302
  ## Product Direction
250
303
 
251
- `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:
252
305
 
253
306
  - `v0.6`: explainability and confidence scoring
254
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
@@ -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", "duplicates", "unused", "outdated", "risks", "suggestions", "topIssues", "config"],
5
+ "required": ["outputVersion", "rootDir", "score", "scoreBreakdown", "policy", "ownershipSummary", "duplicates", "unused", "outdated", "risks", "suggestions", "topIssues", "config"],
6
6
  "additionalProperties": false,
7
7
  "properties": {
8
8
  "recommendation": {
@@ -31,6 +31,17 @@
31
31
  "dependencyType": { "type": "string", "enum": ["dependencies", "devDependencies", "unknown"] }
32
32
  }
33
33
  },
34
+ "ownershipSummary": {
35
+ "type": "object",
36
+ "required": ["duplicates", "unused", "outdated", "risks"],
37
+ "additionalProperties": false,
38
+ "properties": {
39
+ "duplicates": { "type": "number" },
40
+ "unused": { "type": "number" },
41
+ "outdated": { "type": "number" },
42
+ "risks": { "type": "number" }
43
+ }
44
+ },
34
45
  "outputVersion": { "type": "string" },
35
46
  "rootDir": { "type": "string" },
36
47
  "score": { "type": "number" },
@@ -70,11 +81,25 @@
70
81
  "type": "array",
71
82
  "items": {
72
83
  "type": "object",
73
- "required": ["name", "versions", "instances", "confidence", "reasonCodes", "explanation", "recommendation"],
84
+ "required": ["name", "versions", "instances", "workspaceUsage", "rootCause", "confidence", "reasonCodes", "explanation", "recommendation"],
74
85
  "additionalProperties": false,
75
86
  "properties": {
76
87
  "name": { "type": "string" },
77
88
  "versions": { "type": "array", "items": { "type": "string" } },
89
+ "workspaceUsage": {
90
+ "type": "array",
91
+ "items": {
92
+ "type": "object",
93
+ "required": ["workspace", "section", "declaredVersion"],
94
+ "additionalProperties": false,
95
+ "properties": {
96
+ "workspace": { "type": "string" },
97
+ "section": { "type": "string", "enum": ["dependencies", "devDependencies"] },
98
+ "declaredVersion": { "type": "string" }
99
+ }
100
+ }
101
+ },
102
+ "rootCause": { "type": "array", "items": { "type": "string" } },
78
103
  "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
79
104
  "reasonCodes": { "type": "array", "items": { "type": "string" } },
80
105
  "explanation": { "type": "array", "items": { "type": "string" } },
@@ -4,6 +4,8 @@ export async function findDuplicateDependencies(graph) {
4
4
  name,
5
5
  versions: Array.from(new Set(instances.map((instance) => instance.version))).sort(),
6
6
  instances,
7
+ workspaceUsage: [],
8
+ rootCause: [],
7
9
  confidence: 0.98,
8
10
  reasonCodes: [
9
11
  "multiple_lockfile_versions",
@@ -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,11 +3,29 @@ 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;
9
16
  version: string;
10
17
  }
18
+ export interface WorkspaceDependencyUsage {
19
+ workspace: string;
20
+ section: "dependencies" | "devDependencies";
21
+ declaredVersion: string;
22
+ }
23
+ export interface WorkspaceOwnershipSummary {
24
+ duplicates: number;
25
+ unused: number;
26
+ outdated: number;
27
+ risks: number;
28
+ }
11
29
  export interface Recommendation {
12
30
  action: "remove" | "consolidate" | "upgrade" | "review";
13
31
  priority: "high" | "medium" | "low";
@@ -29,6 +47,8 @@ export interface DuplicateDependency {
29
47
  name: string;
30
48
  versions: string[];
31
49
  instances: DuplicateInstance[];
50
+ workspaceUsage: WorkspaceDependencyUsage[];
51
+ rootCause: string[];
32
52
  confidence: number;
33
53
  reasonCodes: string[];
34
54
  explanation: string[];
@@ -81,6 +101,7 @@ export interface AnalysisResult {
81
101
  score: number;
82
102
  scoreBreakdown: ScoreBreakdown;
83
103
  policy: PolicyResult;
104
+ ownershipSummary: WorkspaceOwnershipSummary;
84
105
  duplicates: DuplicateDependency[];
85
106
  unused: UnusedDependency[];
86
107
  outdated: OutdatedDependency[];
@@ -100,6 +121,7 @@ export interface PackageAnalysisResult {
100
121
  score: number;
101
122
  scoreBreakdown: ScoreBreakdown;
102
123
  policy: PolicyResult;
124
+ ownershipSummary: WorkspaceOwnershipSummary;
103
125
  duplicates: DuplicateDependency[];
104
126
  unused: UnusedDependency[];
105
127
  outdated: OutdatedDependency[];
@@ -107,7 +129,7 @@ export interface PackageAnalysisResult {
107
129
  suggestions: string[];
108
130
  topIssues: TopIssue[];
109
131
  }
110
- export declare const OUTPUT_VERSION = "1.3";
132
+ export declare const OUTPUT_VERSION = "1.4";
111
133
  export interface ScoreBreakdown {
112
134
  baseScore: number;
113
135
  duplicates: number;
@@ -8,14 +8,16 @@ import { findWorkspacePackages } from "../utils/workspaces.js";
8
8
  import { buildDependencyGraph } from "./graph-builder.js";
9
9
  import { calculateHealthScore } from "./scorer.js";
10
10
  import { buildAnalysisContext } from "./context.js";
11
- export const OUTPUT_VERSION = "1.3";
11
+ export const OUTPUT_VERSION = "1.4";
12
12
  export async function analyzeProject(options = {}) {
13
13
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
14
14
  const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
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);
@@ -28,11 +30,26 @@ export async function analyzeProject(options = {}) {
28
30
  });
29
31
  packages.push({ ...result, name: workspace.name });
30
32
  }
31
- const unused = packages.flatMap((pkg) => pkg.unused.map((item) => ({ ...item, package: pkg.name })));
32
- const outdated = packages.flatMap((pkg) => pkg.outdated.map((item) => ({ ...item, package: pkg.name })));
33
- const risks = packages.flatMap((pkg) => pkg.risks.map((item) => ({ ...item, package: pkg.name })));
33
+ const workspaceGraphs = await Promise.all(workspaces.map(async (workspace) => ({
34
+ name: workspace.name,
35
+ graph: await buildDependencyGraph(workspace.rootDir)
36
+ })));
37
+ const attributedDuplicates = addWorkspaceAttribution(duplicates, workspaceGraphs);
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;
34
51
  const score = calculateHealthScore({
35
- duplicates: duplicates.length,
52
+ duplicates: activeDuplicates.length,
36
53
  unused: unused.length,
37
54
  outdated: outdated.length,
38
55
  risks: risks.length,
@@ -42,17 +59,19 @@ export async function analyzeProject(options = {}) {
42
59
  riskWeight: config.scoring.riskWeight
43
60
  });
44
61
  const scoreBreakdown = buildScoreBreakdown({
45
- duplicates: duplicates.length,
62
+ duplicates: activeDuplicates.length,
46
63
  unused: unused.length,
47
64
  outdated: outdated.length,
48
65
  risks: risks.length
49
66
  }, config);
50
67
  const suggestions = [
51
- ...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})`)
52
71
  ].slice(0, config.report.maxSuggestions);
53
72
  const policy = evaluatePolicy({
54
73
  score,
55
- duplicates: duplicates.length,
74
+ duplicates: activeDuplicates.length,
56
75
  unused: unused.length,
57
76
  outdated: outdated.length,
58
77
  risks: risks.length
@@ -63,12 +82,23 @@ export async function analyzeProject(options = {}) {
63
82
  score,
64
83
  scoreBreakdown,
65
84
  policy,
66
- duplicates,
85
+ ownershipSummary: buildOwnershipSummary({
86
+ duplicates: activeDuplicates,
87
+ unused,
88
+ outdated,
89
+ risks
90
+ }),
91
+ duplicates: activeDuplicates,
67
92
  unused,
68
93
  outdated,
69
94
  risks,
70
95
  suggestions,
71
- topIssues: buildTopIssues({ duplicates, unused, outdated, risks }),
96
+ topIssues: buildTopIssues({
97
+ duplicates: activeDuplicates,
98
+ unused,
99
+ outdated,
100
+ risks
101
+ }),
72
102
  config,
73
103
  packages
74
104
  };
@@ -139,59 +169,71 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
139
169
  const unused = mapUnusedIssues(issueGroups.unused);
140
170
  const outdated = mapOutdatedIssues(issueGroups.outdated);
141
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);
142
187
  const score = calculateHealthScore({
143
- duplicates: duplicates.length,
144
- unused: unused.length,
145
- outdated: outdated.length,
146
- risks: risks.length,
188
+ duplicates: baselineFiltered.duplicates.length,
189
+ unused: baselineFiltered.unused.length,
190
+ outdated: baselineFiltered.outdated.length,
191
+ risks: baselineFiltered.risks.length,
147
192
  duplicateWeight: config.scoring.duplicateWeight,
148
193
  outdatedWeight: config.scoring.outdatedWeight,
149
194
  unusedWeight: config.scoring.unusedWeight,
150
195
  riskWeight: config.scoring.riskWeight
151
196
  });
152
197
  const scoreBreakdown = buildScoreBreakdown({
153
- duplicates: duplicates.length,
154
- unused: unused.length,
155
- outdated: outdated.length,
156
- risks: risks.length
198
+ duplicates: baselineFiltered.duplicates.length,
199
+ unused: baselineFiltered.unused.length,
200
+ outdated: baselineFiltered.outdated.length,
201
+ risks: baselineFiltered.risks.length
157
202
  }, config);
158
203
  const suggestions = [
159
- ...unused.map((item) => `Remove ${item.name} from ${item.section}`),
160
- ...duplicates.map((item) => `Consider consolidating ${item.name} to one version`),
161
- ...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})`)
162
207
  ].slice(0, config.report.maxSuggestions);
163
208
  const policy = evaluatePolicy({
164
209
  score,
165
- duplicates: duplicates.length,
166
- unused: unused.length,
167
- outdated: outdated.length,
168
- risks: risks.length
210
+ duplicates: baselineFiltered.duplicates.length,
211
+ unused: baselineFiltered.unused.length,
212
+ outdated: baselineFiltered.outdated.length,
213
+ risks: baselineFiltered.risks.length
169
214
  }, config);
170
- const scopedUnused = options.packageName && options.packageName.trim().length > 0
171
- ? unused.map((item) => ({ ...item, package: options.packageName }))
172
- : unused;
173
- const scopedOutdated = options.packageName && options.packageName.trim().length > 0
174
- ? outdated.map((item) => ({ ...item, package: options.packageName }))
175
- : outdated;
176
- const scopedRisks = options.packageName && options.packageName.trim().length > 0
177
- ? risks.map((item) => ({ ...item, package: options.packageName }))
178
- : risks;
179
215
  return {
180
216
  outputVersion: OUTPUT_VERSION,
181
217
  rootDir,
182
218
  score,
183
219
  scoreBreakdown,
184
220
  policy,
185
- duplicates,
186
- unused: scopedUnused,
187
- outdated: scopedOutdated,
188
- risks: scopedRisks,
221
+ ownershipSummary: buildOwnershipSummary({
222
+ duplicates: baselineFiltered.duplicates,
223
+ unused: baselineFiltered.unused,
224
+ outdated: baselineFiltered.outdated,
225
+ risks: baselineFiltered.risks
226
+ }),
227
+ duplicates: baselineFiltered.duplicates,
228
+ unused: baselineFiltered.unused,
229
+ outdated: baselineFiltered.outdated,
230
+ risks: baselineFiltered.risks,
189
231
  suggestions,
190
232
  topIssues: buildTopIssues({
191
- duplicates,
192
- unused: scopedUnused,
193
- outdated: scopedOutdated,
194
- risks: scopedRisks
233
+ duplicates: baselineFiltered.duplicates,
234
+ unused: baselineFiltered.unused,
235
+ outdated: baselineFiltered.outdated,
236
+ risks: baselineFiltered.risks
195
237
  }),
196
238
  config
197
239
  };
@@ -272,6 +314,8 @@ function mapDuplicateIssues(issues) {
272
314
  instances: Array.isArray(issue.meta?.instances)
273
315
  ? issue.meta?.instances
274
316
  : [],
317
+ workspaceUsage: normalizeWorkspaceUsage(issue.meta?.workspaceUsage),
318
+ rootCause: normalizeStringArray(issue.meta?.rootCause),
275
319
  confidence: normalizeConfidence(issue.confidence),
276
320
  reasonCodes: normalizeStringArray(issue.reasonCodes),
277
321
  explanation: normalizeStringArray(issue.explanation),
@@ -437,6 +481,62 @@ function compareTrustScore(left, right) {
437
481
  const rank = { low: 3, medium: 2, high: 1, undefined: 0 };
438
482
  return rank[left ?? "undefined"] - rank[right ?? "undefined"];
439
483
  }
484
+ function addWorkspaceAttribution(duplicates, workspaceGraphs) {
485
+ return duplicates.map((item) => {
486
+ const usage = [];
487
+ for (const workspace of workspaceGraphs) {
488
+ const runtimeVersion = workspace.graph.dependencies[item.name];
489
+ if (runtimeVersion) {
490
+ usage.push({
491
+ workspace: workspace.name,
492
+ section: "dependencies",
493
+ declaredVersion: runtimeVersion
494
+ });
495
+ }
496
+ const devVersion = workspace.graph.devDependencies[item.name];
497
+ if (devVersion) {
498
+ usage.push({
499
+ workspace: workspace.name,
500
+ section: "devDependencies",
501
+ declaredVersion: devVersion
502
+ });
503
+ }
504
+ }
505
+ return {
506
+ ...item,
507
+ workspaceUsage: usage,
508
+ rootCause: usage.map((entry) => `${entry.workspace} -> ${item.name}@${entry.declaredVersion}`)
509
+ };
510
+ });
511
+ }
512
+ function buildOwnershipSummary(input) {
513
+ return {
514
+ duplicates: input.duplicates.length,
515
+ unused: input.unused.length,
516
+ outdated: input.outdated.length,
517
+ risks: input.risks.length
518
+ };
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
+ }
440
540
  function normalizeConfidence(value) {
441
541
  if (typeof value !== "number" || Number.isNaN(value)) {
442
542
  return 0.5;
@@ -479,6 +579,29 @@ function normalizeRiskFactors(value) {
479
579
  : "unknown"
480
580
  };
481
581
  }
582
+ function normalizeWorkspaceUsage(value) {
583
+ if (!Array.isArray(value)) {
584
+ return [];
585
+ }
586
+ return value
587
+ .map((entry) => {
588
+ if (!entry || typeof entry !== "object") {
589
+ return null;
590
+ }
591
+ const usage = entry;
592
+ if (typeof usage.workspace !== "string" ||
593
+ typeof usage.declaredVersion !== "string" ||
594
+ (usage.section !== "dependencies" && usage.section !== "devDependencies")) {
595
+ return null;
596
+ }
597
+ return {
598
+ workspace: usage.workspace,
599
+ section: usage.section,
600
+ declaredVersion: usage.declaredVersion
601
+ };
602
+ })
603
+ .filter((entry) => entry !== null);
604
+ }
482
605
  function buildScoreBreakdown(counts, config) {
483
606
  return {
484
607
  baseScore: 100,
@@ -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, ScoreBreakdown, RiskDependency, TopIssue, UnusedDependency } 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";
@@ -20,10 +20,10 @@ export function renderConsoleReport(result) {
20
20
  lines.push("");
21
21
  lines.push("Packages:");
22
22
  for (const pkg of result.packages) {
23
- lines.push(`- ${pkg.name}: ${pkg.score}/100, D:${pkg.duplicates.length} U:${pkg.unused.length} O:${pkg.outdated.length} R:${pkg.risks.length}`);
23
+ lines.push(`- ${pkg.name}: ${pkg.score}/100, D:${pkg.ownershipSummary.duplicates} U:${pkg.ownershipSummary.unused} O:${pkg.ownershipSummary.outdated} R:${pkg.ownershipSummary.risks}`);
24
24
  }
25
25
  }
26
- appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation, item.recommendation)));
26
+ appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}${item.rootCause.length > 0 ? ` | via ${item.rootCause.join("; ")}` : ""}`, item.confidence, item.explanation, item.recommendation)));
27
27
  appendSection(lines, "Unused dependencies", result.unused.map((item) => formatEntry(item.package
28
28
  ? `${item.name} (${item.section}) [${item.package}]`
29
29
  : `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
@@ -17,7 +17,7 @@ export function renderMarkdownReport(result) {
17
17
  if (result.packages && result.packages.length > 0) {
18
18
  lines.push("## Packages");
19
19
  for (const pkg of result.packages) {
20
- lines.push(`- ${pkg.name}: ${pkg.score}/100 (D:${pkg.duplicates.length} U:${pkg.unused.length} O:${pkg.outdated.length} R:${pkg.risks.length})`);
20
+ lines.push(`- ${pkg.name}: ${pkg.score}/100 (D:${pkg.ownershipSummary.duplicates} U:${pkg.ownershipSummary.unused} O:${pkg.ownershipSummary.outdated} R:${pkg.ownershipSummary.risks})`);
21
21
  }
22
22
  lines.push("");
23
23
  }
@@ -27,7 +27,7 @@ export function renderMarkdownReport(result) {
27
27
  lines.push(`- Outdated: ${result.outdated.length}`);
28
28
  lines.push(`- Risks: ${result.risks.length}`);
29
29
  lines.push("");
30
- appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation, item.recommendation)));
30
+ appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}${item.rootCause.length > 0 ? ` | via ${item.rootCause.join("; ")}` : ""}`, item.confidence, item.explanation, item.recommendation)));
31
31
  appendSection(lines, "Unused dependencies", result.unused.map((item) => formatEntry(item.package
32
32
  ? `${item.name} (${item.section}) [${item.package}]`
33
33
  : `${item.name} (${item.section})`, item.confidence, item.explanation, item.recommendation)));
@@ -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.8.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"