dep-brain 0.5.2 → 0.6.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/README.md CHANGED
@@ -4,11 +4,16 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/dep-brain)](https://www.npmjs.com/package/dep-brain)
5
5
  [![license](https://img.shields.io/npm/l/dep-brain)](LICENSE)
6
6
 
7
- `dep-brain` is a CLI and library for analyzing dependency health in JavaScript and TypeScript projects.
7
+ `dep-brain` is a CLI and library for explainable dependency intelligence in JavaScript and TypeScript projects.
8
8
 
9
9
  ## Vision
10
10
 
11
- `npm audit + depcheck + dedupe + intelligence = one tool`
11
+ `dep-brain` aims to become a dependency decision engine:
12
+
13
+ - Explain why a dependency matters
14
+ - Evaluate how safe, risky, or necessary it is
15
+ - Recommend what to do next
16
+ - Enforce decisions in CI workflows
12
17
 
13
18
  ## What It Does
14
19
 
@@ -19,6 +24,12 @@
19
24
  - Generate a simple project health score
20
25
  - Output reports in human-readable or JSON format
21
26
 
27
+ The long-term goal is not just to list problems, but to answer:
28
+
29
+ - Why is this dependency here?
30
+ - Can I remove it safely?
31
+ - What should I fix first?
32
+
22
33
  ## Current MVP Features
23
34
 
24
35
  - Duplicate dependency detection with lockfile instance tracking
@@ -225,14 +236,20 @@ src/
225
236
  `-- config.ts
226
237
  ```
227
238
 
228
- ## Roadmap Direction
239
+ ## Product Direction
240
+
241
+ `dep-brain` is currently in the `v0.5.x` foundation stage. The next roadmap is:
242
+
243
+ - `v0.6`: explainability and confidence scoring
244
+ - `v0.7`: safe removal guidance and actionable recommendations
245
+ - `v0.8`: supply-chain trust and risk intelligence
246
+ - `v0.9`: deeper monorepo and ownership intelligence
247
+ - `v1.0`: stable CI, ecosystem exports, and production readiness
229
248
 
230
- - Improve false-positive reduction for unused dependency detection
231
- - Improve monorepo and workspace support
232
- - Strengthen risk scoring and suggestions
233
- - Add CI and GitHub Action support in later releases
249
+ The project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
234
250
 
235
251
  ## Repository Notes
236
252
 
237
253
  - Project brief: [docs/project-brief.md](./docs/project-brief.md)
254
+ - Product roadmap: [docs/product-roadmap.md](./docs/product-roadmap.md)
238
255
  - Implementation history: [docs/implementation-log.md](./docs/implementation-log.md)
@@ -44,11 +44,14 @@
44
44
  "type": "array",
45
45
  "items": {
46
46
  "type": "object",
47
- "required": ["name", "versions", "instances"],
47
+ "required": ["name", "versions", "instances", "confidence", "reasonCodes", "explanation"],
48
48
  "additionalProperties": false,
49
49
  "properties": {
50
50
  "name": { "type": "string" },
51
51
  "versions": { "type": "array", "items": { "type": "string" } },
52
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
53
+ "reasonCodes": { "type": "array", "items": { "type": "string" } },
54
+ "explanation": { "type": "array", "items": { "type": "string" } },
52
55
  "instances": {
53
56
  "type": "array",
54
57
  "items": {
@@ -68,12 +71,15 @@
68
71
  "type": "array",
69
72
  "items": {
70
73
  "type": "object",
71
- "required": ["name", "section"],
74
+ "required": ["name", "section", "confidence", "reasonCodes", "explanation"],
72
75
  "additionalProperties": false,
73
76
  "properties": {
74
77
  "name": { "type": "string" },
75
78
  "section": { "type": "string", "enum": ["dependencies", "devDependencies"] },
76
- "package": { "type": "string" }
79
+ "package": { "type": "string" },
80
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
81
+ "reasonCodes": { "type": "array", "items": { "type": "string" } },
82
+ "explanation": { "type": "array", "items": { "type": "string" } }
77
83
  }
78
84
  }
79
85
  },
@@ -81,14 +87,17 @@
81
87
  "type": "array",
82
88
  "items": {
83
89
  "type": "object",
84
- "required": ["name", "current", "latest", "updateType"],
90
+ "required": ["name", "current", "latest", "updateType", "confidence", "reasonCodes", "explanation"],
85
91
  "additionalProperties": false,
86
92
  "properties": {
87
93
  "name": { "type": "string" },
88
94
  "current": { "type": "string" },
89
95
  "latest": { "type": "string" },
90
96
  "updateType": { "type": "string" },
91
- "package": { "type": "string" }
97
+ "package": { "type": "string" },
98
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
99
+ "reasonCodes": { "type": "array", "items": { "type": "string" } },
100
+ "explanation": { "type": "array", "items": { "type": "string" } }
92
101
  }
93
102
  }
94
103
  },
@@ -96,12 +105,15 @@
96
105
  "type": "array",
97
106
  "items": {
98
107
  "type": "object",
99
- "required": ["name", "reasons"],
108
+ "required": ["name", "reasons", "confidence", "reasonCodes", "explanation"],
100
109
  "additionalProperties": false,
101
110
  "properties": {
102
111
  "name": { "type": "string" },
103
112
  "reasons": { "type": "array", "items": { "type": "string" } },
104
- "package": { "type": "string" }
113
+ "package": { "type": "string" },
114
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
115
+ "reasonCodes": { "type": "array", "items": { "type": "string" } },
116
+ "explanation": { "type": "array", "items": { "type": "string" } }
105
117
  }
106
118
  }
107
119
  },
@@ -3,7 +3,16 @@ export async function findDuplicateDependencies(graph) {
3
3
  .map(([name, instances]) => ({
4
4
  name,
5
5
  versions: Array.from(new Set(instances.map((instance) => instance.version))).sort(),
6
- instances
6
+ instances,
7
+ confidence: 0.98,
8
+ reasonCodes: [
9
+ "multiple_lockfile_versions",
10
+ "multiple_installation_paths"
11
+ ],
12
+ explanation: [
13
+ `Multiple versions of ${name} were found in the lockfile.`,
14
+ "The package is installed from more than one dependency path."
15
+ ]
7
16
  }))
8
17
  .filter((dependency) => dependency.versions.length > 1)
9
18
  .sort((left, right) => left.name.localeCompare(right.name));
@@ -17,6 +26,9 @@ export async function runDuplicateCheck(graph) {
17
26
  id: `duplicate:${item.name}`,
18
27
  message: `${item.name} has ${item.versions.length} versions`,
19
28
  severity: "warning",
29
+ confidence: item.confidence,
30
+ reasonCodes: item.reasonCodes,
31
+ explanation: item.explanation,
20
32
  meta: {
21
33
  name: item.name,
22
34
  versions: item.versions,
@@ -15,7 +15,16 @@ export async function findOutdatedDependencies(graph, options = {}) {
15
15
  name,
16
16
  current,
17
17
  latest,
18
- updateType: classifyUpdateType(normalized, latest)
18
+ updateType: classifyUpdateType(normalized, latest),
19
+ confidence: 0.97,
20
+ reasonCodes: [
21
+ "latest_registry_version_newer",
22
+ `update_type_${classifyUpdateType(normalized, latest)}`
23
+ ],
24
+ explanation: [
25
+ "The npm registry reports a newer published version than the one declared in this project.",
26
+ `The change is classified as a ${classifyUpdateType(normalized, latest)} update.`
27
+ ]
19
28
  };
20
29
  }));
21
30
  return results
@@ -31,6 +40,9 @@ export async function runOutdatedCheck(graph) {
31
40
  id: `outdated:${item.name}`,
32
41
  message: `${item.name} ${item.current} -> ${item.latest}`,
33
42
  severity: item.updateType === "major" ? "critical" : "warning",
43
+ confidence: item.confidence,
44
+ reasonCodes: item.reasonCodes,
45
+ explanation: item.explanation,
34
46
  meta: {
35
47
  name: item.name,
36
48
  current: item.current,
@@ -23,7 +23,13 @@ export async function findRiskDependencies(graph) {
23
23
  if (reasons.length === 0) {
24
24
  return null;
25
25
  }
26
- return { name, reasons };
26
+ return {
27
+ name,
28
+ reasons,
29
+ confidence: calculateRiskConfidence(reasons),
30
+ reasonCodes: reasons.map(toRiskReasonCode),
31
+ explanation: reasons
32
+ };
27
33
  }));
28
34
  return results
29
35
  .filter((item) => item !== null)
@@ -38,6 +44,9 @@ export async function runRiskCheck(graph) {
38
44
  id: `risk:${item.name}`,
39
45
  message: `${item.name}: ${item.reasons.join("; ")}`,
40
46
  severity: "warning",
47
+ confidence: item.confidence,
48
+ reasonCodes: item.reasonCodes,
49
+ explanation: item.explanation,
41
50
  meta: {
42
51
  name: item.name,
43
52
  reasons: item.reasons
@@ -45,3 +54,13 @@ export async function runRiskCheck(graph) {
45
54
  }))
46
55
  };
47
56
  }
57
+ function calculateRiskConfidence(reasons) {
58
+ return Math.min(0.99, 0.55 + reasons.length * 0.12);
59
+ }
60
+ function toRiskReasonCode(reason) {
61
+ const normalized = reason
62
+ .toLowerCase()
63
+ .replace(/[^a-z0-9]+/g, "_")
64
+ .replace(/^_+|_+$/g, "");
65
+ return normalized || "risk_signal_detected";
66
+ }
@@ -33,11 +33,11 @@ export async function findUnusedDependencies(rootDir, graph, fileEntries, option
33
33
  }
34
34
  const unusedDependencies = Object.keys(graph.dependencies)
35
35
  .filter((name) => !runtimeUsed.has(name))
36
- .map((name) => ({ name, section: "dependencies" }));
36
+ .map((name) => buildUnusedDependency(name, "dependencies"));
37
37
  const unusedDevDependencies = Object.keys(graph.devDependencies)
38
38
  .filter((name) => !devUsed.has(name) && !runtimeUsed.has(name))
39
39
  .filter((name) => !isImplicitlyUsedDevDependency(name, hasTypeScriptSources, options.hasTypeScriptConfig))
40
- .map((name) => ({ name, section: "devDependencies" }));
40
+ .map((name) => buildUnusedDependency(name, "devDependencies"));
41
41
  return [...unusedDependencies, ...unusedDevDependencies].sort((left, right) => left.name.localeCompare(right.name));
42
42
  }
43
43
  export async function runUnusedCheck(context) {
@@ -49,6 +49,9 @@ export async function runUnusedCheck(context) {
49
49
  id: `unused:${item.section}:${item.name}`,
50
50
  message: `${item.name} appears unused`,
51
51
  severity: "warning",
52
+ confidence: item.confidence,
53
+ reasonCodes: item.reasonCodes,
54
+ explanation: item.explanation,
52
55
  meta: {
53
56
  name: item.name,
54
57
  section: item.section
@@ -127,3 +130,20 @@ function isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScript
127
130
  }
128
131
  return false;
129
132
  }
133
+ function buildUnusedDependency(name, section) {
134
+ return {
135
+ name,
136
+ section,
137
+ confidence: section === "dependencies" ? 0.9 : 0.82,
138
+ reasonCodes: [
139
+ "no_source_import_found",
140
+ "no_config_reference_found",
141
+ "no_script_reference_found"
142
+ ],
143
+ explanation: [
144
+ "No import or require usage was found in scanned source files.",
145
+ "No matching reference was found in recognized config files.",
146
+ "No matching binary or package reference was found in package scripts."
147
+ ]
148
+ };
149
+ }
@@ -12,11 +12,17 @@ export interface DuplicateDependency {
12
12
  name: string;
13
13
  versions: string[];
14
14
  instances: DuplicateInstance[];
15
+ confidence: number;
16
+ reasonCodes: string[];
17
+ explanation: string[];
15
18
  }
16
19
  export interface UnusedDependency {
17
20
  name: string;
18
21
  section: "dependencies" | "devDependencies";
19
22
  package?: string;
23
+ confidence: number;
24
+ reasonCodes: string[];
25
+ explanation: string[];
20
26
  }
21
27
  export interface OutdatedDependency {
22
28
  name: string;
@@ -24,11 +30,17 @@ export interface OutdatedDependency {
24
30
  latest: string;
25
31
  updateType: "major" | "minor" | "patch" | "unknown";
26
32
  package?: string;
33
+ confidence: number;
34
+ reasonCodes: string[];
35
+ explanation: string[];
27
36
  }
28
37
  export interface RiskDependency {
29
38
  name: string;
30
39
  reasons: string[];
31
40
  package?: string;
41
+ confidence: number;
42
+ reasonCodes: string[];
43
+ explanation: string[];
32
44
  }
33
45
  export interface AnalysisResult {
34
46
  outputVersion: string;
@@ -60,7 +72,7 @@ export interface PackageAnalysisResult {
60
72
  risks: RiskDependency[];
61
73
  suggestions: string[];
62
74
  }
63
- export declare const OUTPUT_VERSION = "1.0";
75
+ export declare const OUTPUT_VERSION = "1.1";
64
76
  export interface ScoreBreakdown {
65
77
  baseScore: number;
66
78
  duplicates: number;
@@ -8,7 +8,7 @@ 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.0";
11
+ export const OUTPUT_VERSION = "1.1";
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);
@@ -264,13 +264,19 @@ function mapDuplicateIssues(issues) {
264
264
  versions: Array.isArray(issue.meta?.versions) ? issue.meta?.versions : [],
265
265
  instances: Array.isArray(issue.meta?.instances)
266
266
  ? issue.meta?.instances
267
- : []
267
+ : [],
268
+ confidence: normalizeConfidence(issue.confidence),
269
+ reasonCodes: normalizeStringArray(issue.reasonCodes),
270
+ explanation: normalizeStringArray(issue.explanation)
268
271
  }));
269
272
  }
270
273
  function mapUnusedIssues(issues) {
271
274
  return issues.map((issue) => ({
272
275
  name: String(issue.meta?.name ?? issue.package ?? "unknown"),
273
- section: issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies"
276
+ section: issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies",
277
+ confidence: normalizeConfidence(issue.confidence),
278
+ reasonCodes: normalizeStringArray(issue.reasonCodes),
279
+ explanation: normalizeStringArray(issue.explanation)
274
280
  }));
275
281
  }
276
282
  function mapOutdatedIssues(issues) {
@@ -280,15 +286,33 @@ function mapOutdatedIssues(issues) {
280
286
  latest: String(issue.meta?.latest ?? ""),
281
287
  updateType: issue.meta?.updateType === "major" || issue.meta?.updateType === "minor" || issue.meta?.updateType === "patch"
282
288
  ? issue.meta.updateType
283
- : "unknown"
289
+ : "unknown",
290
+ confidence: normalizeConfidence(issue.confidence),
291
+ reasonCodes: normalizeStringArray(issue.reasonCodes),
292
+ explanation: normalizeStringArray(issue.explanation)
284
293
  }));
285
294
  }
286
295
  function mapRiskIssues(issues) {
287
296
  return issues.map((issue) => ({
288
297
  name: String(issue.meta?.name ?? issue.package ?? "unknown"),
289
- reasons: Array.isArray(issue.meta?.reasons) ? issue.meta?.reasons : []
298
+ reasons: Array.isArray(issue.meta?.reasons) ? issue.meta?.reasons : [],
299
+ confidence: normalizeConfidence(issue.confidence),
300
+ reasonCodes: normalizeStringArray(issue.reasonCodes),
301
+ explanation: normalizeStringArray(issue.explanation)
290
302
  }));
291
303
  }
304
+ function normalizeConfidence(value) {
305
+ if (typeof value !== "number" || Number.isNaN(value)) {
306
+ return 0.5;
307
+ }
308
+ return Math.min(0.99, Math.max(0, Number(value.toFixed(2))));
309
+ }
310
+ function normalizeStringArray(value) {
311
+ if (!Array.isArray(value)) {
312
+ return [];
313
+ }
314
+ return value.filter((entry) => typeof entry === "string");
315
+ }
292
316
  function buildScoreBreakdown(counts, config) {
293
317
  return {
294
318
  baseScore: 100,
@@ -1,9 +1,13 @@
1
1
  export type IssueSeverity = "info" | "warning" | "critical";
2
+ export type ReasonCode = string;
2
3
  export type Issue = {
3
4
  id: string;
4
5
  message: string;
5
6
  package?: string;
6
7
  severity: IssueSeverity;
8
+ confidence?: number;
9
+ reasonCodes?: ReasonCode[];
10
+ explanation?: string[];
7
11
  meta?: Record<string, unknown>;
8
12
  };
9
13
  export type CheckResult = {
@@ -16,16 +16,16 @@ export function renderConsoleReport(result) {
16
16
  lines.push(`- ${pkg.name}: ${pkg.score}/100, D:${pkg.duplicates.length} U:${pkg.unused.length} O:${pkg.outdated.length} R:${pkg.risks.length}`);
17
17
  }
18
18
  }
19
- appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => `${item.name}: ${item.versions.join(", ")}`));
20
- appendSection(lines, "Unused dependencies", result.unused.map((item) => item.package
19
+ appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation)));
20
+ appendSection(lines, "Unused dependencies", result.unused.map((item) => formatEntry(item.package
21
21
  ? `${item.name} (${item.section}) [${item.package}]`
22
- : `${item.name} (${item.section})`));
23
- appendSection(lines, "Outdated dependencies", result.outdated.map((item) => item.package
22
+ : `${item.name} (${item.section})`, item.confidence, item.explanation)));
23
+ appendSection(lines, "Outdated dependencies", result.outdated.map((item) => formatEntry(item.package
24
24
  ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
25
- : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`));
26
- appendSection(lines, "Risky dependencies", result.risks.map((item) => item.package
25
+ : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation)));
26
+ appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
27
27
  ? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
28
- : `${item.name}: ${item.reasons.join("; ")}`));
28
+ : `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation)));
29
29
  appendSection(lines, "Policy reasons", result.policy.reasons);
30
30
  if (result.suggestions.length > 0) {
31
31
  lines.push("");
@@ -50,3 +50,7 @@ function appendSection(lines, title, entries) {
50
50
  lines.push(`- ${entry}`);
51
51
  }
52
52
  }
53
+ function formatEntry(label, confidence, explanation) {
54
+ const reasonSummary = explanation.length > 0 ? ` | why: ${explanation.join("; ")}` : "";
55
+ return `${label} | confidence ${Math.round(confidence * 100)}%${reasonSummary}`;
56
+ }
@@ -20,16 +20,16 @@ export function renderMarkdownReport(result) {
20
20
  lines.push(`- Outdated: ${result.outdated.length}`);
21
21
  lines.push(`- Risks: ${result.risks.length}`);
22
22
  lines.push("");
23
- appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => `${item.name}: ${item.versions.join(", ")}`));
24
- appendSection(lines, "Unused dependencies", result.unused.map((item) => item.package
23
+ appendSection(lines, "Duplicate dependencies", result.duplicates.map((item) => formatEntry(`${item.name}: ${item.versions.join(", ")}`, item.confidence, item.explanation)));
24
+ appendSection(lines, "Unused dependencies", result.unused.map((item) => formatEntry(item.package
25
25
  ? `${item.name} (${item.section}) [${item.package}]`
26
- : `${item.name} (${item.section})`));
27
- appendSection(lines, "Outdated dependencies", result.outdated.map((item) => item.package
26
+ : `${item.name} (${item.section})`, item.confidence, item.explanation)));
27
+ appendSection(lines, "Outdated dependencies", result.outdated.map((item) => formatEntry(item.package
28
28
  ? `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}] [${item.package}]`
29
- : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`));
30
- appendSection(lines, "Risky dependencies", result.risks.map((item) => item.package
29
+ : `${item.name}: ${item.current} -> ${item.latest} [${item.updateType}]`, item.confidence, item.explanation)));
30
+ appendSection(lines, "Risky dependencies", result.risks.map((item) => formatEntry(item.package
31
31
  ? `${item.name}: ${item.reasons.join("; ")} [${item.package}]`
32
- : `${item.name}: ${item.reasons.join("; ")}`));
32
+ : `${item.name}: ${item.reasons.join("; ")}`, item.confidence, item.explanation)));
33
33
  appendSection(lines, "Policy reasons", result.policy.reasons);
34
34
  if (result.suggestions.length > 0) {
35
35
  lines.push("## Suggestions");
@@ -50,3 +50,7 @@ function appendSection(lines, title, items) {
50
50
  }
51
51
  lines.push("");
52
52
  }
53
+ function formatEntry(label, confidence, explanation) {
54
+ const reasonSummary = explanation.length > 0 ? ` | why: ${explanation.join("; ")}` : "";
55
+ return `${label} | confidence ${Math.round(confidence * 100)}%${reasonSummary}`;
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "CLI and library for dependency health analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",