dependency-radar 0.3.1 → 0.4.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.
@@ -69,9 +69,235 @@ function formatProjectDir(projectPath) {
69
69
  }
70
70
  return projectPath;
71
71
  }
72
+ function asTrimmedString(value) {
73
+ if (typeof value !== 'string')
74
+ return undefined;
75
+ const trimmed = value.trim();
76
+ return trimmed.length > 0 ? trimmed : undefined;
77
+ }
78
+ function normalizeStringList(value) {
79
+ if (typeof value === 'string') {
80
+ const single = value.trim();
81
+ return single ? [single] : undefined;
82
+ }
83
+ if (!Array.isArray(value))
84
+ return undefined;
85
+ const items = value
86
+ .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
87
+ .filter(Boolean);
88
+ if (items.length === 0)
89
+ return undefined;
90
+ return Array.from(new Set(items));
91
+ }
92
+ function toObjectRecord(value) {
93
+ if (!value || typeof value !== 'object' || Array.isArray(value))
94
+ return undefined;
95
+ return value;
96
+ }
97
+ function hasKeys(value) {
98
+ return Boolean(value && Object.keys(value).length > 0);
99
+ }
100
+ function mergeRecordObjects(...sources) {
101
+ const merged = {};
102
+ for (const source of sources) {
103
+ if (!source)
104
+ continue;
105
+ for (const [key, val] of Object.entries(source)) {
106
+ merged[key] = val;
107
+ }
108
+ }
109
+ return Object.keys(merged).length > 0 ? merged : undefined;
110
+ }
111
+ function normalizeRepository(repository) {
112
+ const direct = asTrimmedString(repository);
113
+ if (direct)
114
+ return direct;
115
+ const asObject = toObjectRecord(repository);
116
+ if (!asObject)
117
+ return undefined;
118
+ const url = asTrimmedString(asObject.url);
119
+ if (url)
120
+ return url;
121
+ const type = asTrimmedString(asObject.type);
122
+ const directory = asTrimmedString(asObject.directory);
123
+ if (type && directory)
124
+ return `${type} (${directory})`;
125
+ return type;
126
+ }
127
+ function extractPackageNameFromSelector(selector) {
128
+ let token = selector.trim();
129
+ if (!token)
130
+ return undefined;
131
+ if (token.includes('>')) {
132
+ const parts = token.split('>').map((part) => part.trim()).filter(Boolean);
133
+ if (parts.length > 0)
134
+ token = parts[parts.length - 1];
135
+ }
136
+ if (token.startsWith('npm:')) {
137
+ token = token.slice(4).trim();
138
+ }
139
+ if (!token)
140
+ return undefined;
141
+ if (token.startsWith('@')) {
142
+ const scopedMatch = token.match(/^@[^/\s]+\/[^@\s]+/);
143
+ return scopedMatch ? scopedMatch[0] : undefined;
144
+ }
145
+ const atIndex = token.indexOf('@');
146
+ const unversioned = atIndex > 0 ? token.slice(0, atIndex) : token;
147
+ const match = unversioned.match(/^[^@\s]+/);
148
+ return match ? match[0] : undefined;
149
+ }
150
+ function collectPolicyPackageNames(entries) {
151
+ if (!entries)
152
+ return undefined;
153
+ const names = new Set();
154
+ for (const selector of Object.keys(entries)) {
155
+ const pkgName = extractPackageNameFromSelector(selector);
156
+ if (pkgName)
157
+ names.add(pkgName);
158
+ }
159
+ if (names.size === 0)
160
+ return undefined;
161
+ return Array.from(names).sort();
162
+ }
163
+ function buildProjectDependencyPolicy(projectPkg, inputPolicy) {
164
+ var _a;
165
+ const projectPkgOverrides = toObjectRecord(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.overrides);
166
+ const projectPkgPnpm = toObjectRecord(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.pnpm);
167
+ const projectPkgPnpmOverrides = toObjectRecord(projectPkgPnpm === null || projectPkgPnpm === void 0 ? void 0 : projectPkgPnpm.overrides);
168
+ const projectPkgResolutions = toObjectRecord(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.resolutions);
169
+ const overrides = mergeRecordObjects(projectPkgOverrides, projectPkgPnpmOverrides, inputPolicy === null || inputPolicy === void 0 ? void 0 : inputPolicy.overrides);
170
+ const resolutions = mergeRecordObjects(projectPkgResolutions, inputPolicy === null || inputPolicy === void 0 ? void 0 : inputPolicy.resolutions);
171
+ const sources = new Set();
172
+ if (hasKeys(projectPkgOverrides))
173
+ sources.add('package.json#overrides');
174
+ if (hasKeys(projectPkgPnpmOverrides))
175
+ sources.add('package.json#pnpm.overrides');
176
+ if (hasKeys(projectPkgResolutions))
177
+ sources.add('package.json#resolutions');
178
+ (_a = inputPolicy === null || inputPolicy === void 0 ? void 0 : inputPolicy.sources) === null || _a === void 0 ? void 0 : _a.forEach((source) => {
179
+ const normalized = source.trim();
180
+ if (normalized)
181
+ sources.add(normalized);
182
+ });
183
+ const policy = {
184
+ ...(overrides ? { overrides } : {}),
185
+ ...(resolutions ? { resolutions } : {})
186
+ };
187
+ return {
188
+ ...(hasKeys(policy.overrides) || hasKeys(policy.resolutions) ? { policy } : {}),
189
+ ...(sources.size > 0 ? { sources: Array.from(sources).sort() } : {})
190
+ };
191
+ }
192
+ function buildProjectDependencyPolicySummary(policy, sources) {
193
+ const overrideCount = (policy === null || policy === void 0 ? void 0 : policy.overrides) ? Object.keys(policy.overrides).length : 0;
194
+ const resolutionCount = (policy === null || policy === void 0 ? void 0 : policy.resolutions) ? Object.keys(policy.resolutions).length : 0;
195
+ if (overrideCount === 0 && resolutionCount === 0)
196
+ return undefined;
197
+ const overriddenPackageNames = collectPolicyPackageNames(policy === null || policy === void 0 ? void 0 : policy.overrides);
198
+ const resolvedPackageNames = collectPolicyPackageNames(policy === null || policy === void 0 ? void 0 : policy.resolutions);
199
+ return {
200
+ hasOverrides: overrideCount > 0,
201
+ overrideCount,
202
+ ...(overriddenPackageNames ? { overriddenPackageNames } : {}),
203
+ hasResolutions: resolutionCount > 0,
204
+ resolutionCount,
205
+ ...(resolvedPackageNames ? { resolvedPackageNames } : {}),
206
+ ...(sources && sources.length > 0 ? { sources } : {})
207
+ };
208
+ }
209
+ function buildProjectMetadata(projectPath, projectPkg, inputPolicy) {
210
+ var _a, _b;
211
+ const { policy, sources } = buildProjectDependencyPolicy(projectPkg, inputPolicy);
212
+ const policySummary = buildProjectDependencyPolicySummary(policy, sources);
213
+ const constraints = {
214
+ ...(normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.os) ? { os: normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.os) } : {}),
215
+ ...(normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.cpu) ? { cpu: normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.cpu) } : {}),
216
+ ...(asTrimmedString((_a = projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.engines) === null || _a === void 0 ? void 0 : _a.node) ? { enginesNode: asTrimmedString((_b = projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.engines) === null || _b === void 0 ? void 0 : _b.node) } : {})
217
+ };
218
+ const hasConstraints = Object.keys(constraints).length > 0;
219
+ return {
220
+ projectDir: formatProjectDir(projectPath),
221
+ ...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.name) ? { name: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.name) } : {}),
222
+ ...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.version) ? { version: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.version) } : {}),
223
+ ...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.description) ? { description: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.description) } : {}),
224
+ ...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.license) ? { license: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.license) } : {}),
225
+ ...(normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.keywords) ? { keywords: normalizeStringList(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.keywords) } : {}),
226
+ ...(asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.homepage) ? { homepage: asTrimmedString(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.homepage) } : {}),
227
+ ...(normalizeRepository(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.repository) ? { repository: normalizeRepository(projectPkg === null || projectPkg === void 0 ? void 0 : projectPkg.repository) } : {}),
228
+ ...(hasConstraints ? { constraints } : {}),
229
+ ...(policy ? { dependencyPolicy: policy } : {}),
230
+ ...(policySummary ? { dependencyPolicySummary: policySummary } : {})
231
+ };
232
+ }
233
+ function getRiskRank(risk) {
234
+ if (risk === 'red')
235
+ return 2;
236
+ if (risk === 'amber')
237
+ return 1;
238
+ return 0;
239
+ }
240
+ function maxRisk(...risks) {
241
+ let highest = 'green';
242
+ for (const risk of risks) {
243
+ if (getRiskRank(risk) > getRiskRank(highest))
244
+ highest = risk;
245
+ }
246
+ return highest;
247
+ }
248
+ function resolveLicenseRisk(licenseInfo) {
249
+ const declaredOrPrimaryRisk = (0, license_1.pickLicenseRisk)(licenseInfo.licenseIds);
250
+ if (licenseInfo.record.status !== 'mismatch')
251
+ return declaredOrPrimaryRisk;
252
+ const inferredRisk = licenseInfo.record.inferred
253
+ ? (0, license_1.pickLicenseRisk)([licenseInfo.record.inferred.spdxId])
254
+ : 'green';
255
+ return maxRisk(declaredOrPrimaryRisk, inferredRisk, 'amber');
256
+ }
257
+ function isWorkspaceLocalVersion(version) {
258
+ const normalized = version.trim().toLowerCase();
259
+ return normalized.startsWith('workspace:') || normalized.startsWith('link:') || normalized.startsWith('file:');
260
+ }
261
+ function isPathWithin(basePath, candidatePath) {
262
+ const normalizedBase = path_1.default.resolve(basePath);
263
+ const normalizedCandidate = path_1.default.resolve(candidatePath);
264
+ return (normalizedCandidate === normalizedBase ||
265
+ normalizedCandidate.startsWith(`${normalizedBase}${path_1.default.sep}`));
266
+ }
267
+ function isWorkspacePackageNode(node, input) {
268
+ var _a, _b, _c;
269
+ if (!input.workspaceEnabled)
270
+ return false;
271
+ if ((_a = input.workspacePackageIds) === null || _a === void 0 ? void 0 : _a.has(node.key))
272
+ return true;
273
+ if (isWorkspaceLocalVersion(node.version))
274
+ return true;
275
+ if ((_b = input.workspaceLocalDependencyNames) === null || _b === void 0 ? void 0 : _b.has(node.name))
276
+ return true;
277
+ if (node.path && input.workspacePackagePaths && input.workspacePackagePaths.size > 0) {
278
+ for (const workspacePath of input.workspacePackagePaths) {
279
+ if (isPathWithin(workspacePath, node.path))
280
+ return true;
281
+ }
282
+ }
283
+ if (((_c = input.workspacePackageNames) === null || _c === void 0 ? void 0 : _c.has(node.name)) && node.depth <= 1) {
284
+ return true;
285
+ }
286
+ return false;
287
+ }
72
288
  async function aggregateData(input) {
73
289
  var _a, _b, _c, _d, _e, _f, _g, _h;
74
290
  const pkg = input.pkgOverride || (await (0, utils_1.readPackageJson)(input.projectPath));
291
+ let projectPkg = input.projectPackageJson;
292
+ if (!projectPkg) {
293
+ try {
294
+ projectPkg = await (0, utils_1.readPackageJson)(input.projectPath);
295
+ }
296
+ catch {
297
+ projectPkg = {};
298
+ }
299
+ }
300
+ const project = buildProjectMetadata(input.projectPath, projectPkg, input.projectDependencyPolicy);
75
301
  // Get git branch
76
302
  const gitBranch = await getGitBranch(input.projectPath);
77
303
  const nodeMap = buildNodeMap((_a = input.npmLsResult) === null || _a === void 0 ? void 0 : _a.data);
@@ -88,7 +314,7 @@ async function aggregateData(input) {
88
314
  const dependencies = {};
89
315
  const licenseCache = new Map();
90
316
  const nodeEngineRanges = [];
91
- const nodes = Array.from(nodeMap.values());
317
+ const nodes = Array.from(nodeMap.values()).filter((node) => !isWorkspacePackageNode(node, input));
92
318
  let directCount = 0;
93
319
  const MAX_TOP_ROOT_PACKAGES = 10; // cap to keep payload size predictable
94
320
  const MAX_TOP_PARENT_PACKAGES = 5; // cap for direct parents to keep payload size predictable
@@ -108,7 +334,7 @@ async function aggregateData(input) {
108
334
  }
109
335
  const vulnerabilities = vulnMap.get(node.name) || emptyVulnSummary();
110
336
  const licenseInfo = buildLicenseInfo(licenseSource.license, licenseSource.licenseText);
111
- const licenseRisk = (0, license_1.pickLicenseRisk)(licenseInfo.licenseIds);
337
+ const licenseRisk = resolveLicenseRisk(licenseInfo);
112
338
  // Calculate root causes (direct dependencies that cause this to be installed)
113
339
  const rootCauses = findRootCauses(node, nodeMap, pkg);
114
340
  const packageInsights = await gatherPackageInsights(node.name, node.version, resolvePaths, packageMetaCache, packageStatCache);
@@ -118,7 +344,7 @@ async function aggregateData(input) {
118
344
  const scope = determineScope(node.name, direct, rootCauses, pkg);
119
345
  const importUsage = usageResult.summary.get(node.name);
120
346
  const runtimeImpact = usageResult.runtimeImpact.get(node.name);
121
- const introduction = determineIntroduction(direct, rootCauses, runtimeImpact);
347
+ const introduction = determineIntroduction(direct, scope, rootCauses, runtimeImpact);
122
348
  const parentIds = Array.from(node.parents).sort();
123
349
  const origins = buildOrigins(rootCauses, parentIds, (_e = input.workspaceUsage) === null || _e === void 0 ? void 0 : _e.get(node.name), input.workspaceEnabled, MAX_TOP_ROOT_PACKAGES, MAX_TOP_PARENT_PACKAGES);
124
350
  const execution = packageInsights.execution;
@@ -141,6 +367,8 @@ async function aggregateData(input) {
141
367
  name: node.name,
142
368
  version: node.version,
143
369
  ...(packageInsights.description ? { description: packageInsights.description } : {}),
370
+ ...(typeof packageInsights.fileCount === 'number' ? { fileCount: packageInsights.fileCount } : {}),
371
+ ...(packageInsights.hasBin ? { hasBin: true } : {}),
144
372
  deprecated: packageInsights.deprecated,
145
373
  links: {
146
374
  npm: `https://www.npmjs.com/package/${node.name}`,
@@ -189,15 +417,13 @@ async function aggregateData(input) {
189
417
  const dependencyCount = nodes.length;
190
418
  const transitiveCount = dependencyCount - directCount;
191
419
  return {
192
- schemaVersion: '1.2',
420
+ schemaVersion: '1.3',
193
421
  generatedAt: new Date().toISOString(),
194
422
  dependencyRadarVersion,
195
423
  git: {
196
424
  branch: gitBranch || ''
197
425
  },
198
- project: {
199
- projectDir: formatProjectDir(input.projectPath)
200
- },
426
+ project,
201
427
  environment: {
202
428
  nodeVersion,
203
429
  runtimeVersion,
@@ -215,7 +441,8 @@ async function aggregateData(input) {
215
441
  ...(input.workspaceType ? { type: input.workspaceType } : {}),
216
442
  ...(typeof input.workspacePackageCount === 'number'
217
443
  ? { packageCount: input.workspacePackageCount }
218
- : {})
444
+ : {}),
445
+ ...(input.workspacePackages ? { workspacePackages: input.workspacePackages } : {})
219
446
  },
220
447
  summary: {
221
448
  dependencyCount,
@@ -662,12 +889,14 @@ function buildUsageSummary(graph, projectPath) {
662
889
  file,
663
890
  count,
664
891
  depth: file.split('/').length,
665
- isTest: isTestFile(file)
892
+ category: classifyFileCategory(file)
666
893
  }));
667
- // Rank: prefer non-test files, then higher import counts, then closer to root.
894
+ // Rank: prefer non-testing files, then higher import counts, then closer to root.
668
895
  entries.sort((a, b) => {
669
- if (a.isTest !== b.isTest)
670
- return a.isTest ? 1 : -1;
896
+ const aIsTest = a.category === 'testing';
897
+ const bIsTest = b.category === 'testing';
898
+ if (aIsTest !== bIsTest)
899
+ return aIsTest ? 1 : -1;
671
900
  if (b.count !== a.count)
672
901
  return b.count - a.count;
673
902
  if (a.depth !== b.depth)
@@ -678,14 +907,15 @@ function buildUsageSummary(graph, projectPath) {
678
907
  fileCount: fileMap.size,
679
908
  topFiles: entries.slice(0, 5).map((entry) => entry.file)
680
909
  });
681
- runtimeImpact.set(dep, determineRuntimeImpactFromFiles(Array.from(fileMap.keys())));
910
+ runtimeImpact.set(dep, determineRuntimeImpactFromFiles(entries));
682
911
  }
683
912
  return { summary, runtimeImpact };
684
913
  }
685
914
  function isDirectDependency(name, pkg) {
686
915
  return Boolean((pkg.dependencies && pkg.dependencies[name]) ||
687
916
  (pkg.devDependencies && pkg.devDependencies[name]) ||
688
- (pkg.optionalDependencies && pkg.optionalDependencies[name]));
917
+ (pkg.optionalDependencies && pkg.optionalDependencies[name]) ||
918
+ (pkg.peerDependencies && pkg.peerDependencies[name]));
689
919
  }
690
920
  function directScopeFromPackage(name, pkg) {
691
921
  if (pkg.dependencies && pkg.dependencies[name])
@@ -756,35 +986,65 @@ function buildOrigins(rootCauses, parentIds, workspaceList, workspaceEnabled, ma
756
986
  return origins;
757
987
  }
758
988
  function isTestFile(file) {
759
- return /(^|\/)(__tests__|__mocks__|test|tests)(\/|$)/.test(file) || /\.(test|spec)\./.test(file);
989
+ return (/(^|\/)(__tests__|__mocks__|test|tests|testing|e2e|cypress|playwright|__snapshots__)(\/|$)/.test(file) ||
990
+ /\.(test|spec|e2e)\./.test(file) ||
991
+ /(^|\/)(jest|vitest|playwright|cypress)\.config\./.test(file) ||
992
+ /(^|\/)\.(jest|vitest|playwright|cypress)(rc|\.config)?(\..*)?$/.test(file));
760
993
  }
761
994
  function isToolingFile(file) {
762
- return /(^|\/)(eslint|prettier|stylelint|commitlint|lint-staged|husky)[^\/]*\./.test(file);
995
+ return (/(^|\/)(eslint|prettier|stylelint|commitlint|lint-staged|husky|renovate|semantic-release|release-it|lefthook|dependabot)[^\/]*\./.test(file) ||
996
+ /(^|\/)\.(eslint|eslintrc|prettier|prettierrc|stylelint|stylelintrc|commitlint|commitlintrc|lint-staged|lintstagedrc|husky|huskyrc|renovate|semantic-release|release-it|lefthook|dependabot)(rc|\.config)?(\..*)?$/.test(file));
763
997
  }
764
998
  function isBuildFile(file) {
765
- return /(^|\/)(webpack|rollup|vite|tsconfig|babel|swc|esbuild|parcel|gulpfile|gruntfile|postcss|tailwind)[^\/]*\./.test(file);
999
+ return (/(^|\/)(webpack|rollup|vite|tsconfig|babel|swc|esbuild|parcel|gulpfile|gruntfile|postcss|tailwind|storybook|rspack|turbo|nx|metro)[^\/]*\./.test(file) ||
1000
+ /(^|\/)scripts\/(build|bundle|compile|release|deploy)(\/|\.|$)/.test(file) ||
1001
+ /(^|\/)\.(webpack|rollup|vite|tsconfig|babel|babelrc|swc|swcrc|esbuild|parcel|postcss|postcssrc|tailwind|storybook|rspack|turbo|nx|metro)(rc|\.config)?(\..*)?$/.test(file));
1002
+ }
1003
+ function classifyFileCategory(file) {
1004
+ if (isTestFile(file))
1005
+ return 'testing';
1006
+ if (isToolingFile(file))
1007
+ return 'tooling';
1008
+ if (isBuildFile(file))
1009
+ return 'build';
1010
+ return 'runtime';
766
1011
  }
767
1012
  function determineRuntimeImpactFromFiles(files) {
768
- const categories = new Set();
769
- for (const file of files) {
770
- if (isTestFile(file)) {
771
- categories.add('testing');
772
- }
773
- else if (isToolingFile(file)) {
774
- categories.add('tooling');
775
- }
776
- else if (isBuildFile(file)) {
777
- categories.add('build');
778
- }
779
- else {
780
- categories.add('runtime');
781
- }
782
- }
783
- if (categories.size === 0)
1013
+ const weights = {
1014
+ runtime: 0,
1015
+ build: 0,
1016
+ testing: 0,
1017
+ tooling: 0
1018
+ };
1019
+ let total = 0;
1020
+ for (const entry of files) {
1021
+ const category = classifyFileCategory(entry.file);
1022
+ const weight = Number.isFinite(entry.count) && entry.count > 0 ? entry.count : 1;
1023
+ weights[category] += weight;
1024
+ total += weight;
1025
+ }
1026
+ if (total <= 0)
784
1027
  return 'runtime';
785
- if (categories.size > 1)
786
- return 'mixed';
787
- return Array.from(categories)[0];
1028
+ const ranked = Object.entries(weights)
1029
+ .filter(([, weight]) => weight > 0)
1030
+ .sort((a, b) => b[1] - a[1]);
1031
+ if (ranked.length === 0)
1032
+ return 'runtime';
1033
+ if (ranked.length === 1)
1034
+ return ranked[0][0];
1035
+ const [top, second] = ranked;
1036
+ const topRatio = top[1] / total;
1037
+ const secondRatio = second ? second[1] / total : 0;
1038
+ // Strong dominance: classify as a single category instead of defaulting to mixed.
1039
+ if (topRatio >= 0.7)
1040
+ return top[0];
1041
+ // Runtime is common; tolerate a small amount of non-runtime usage before calling it mixed.
1042
+ if (top[0] === 'runtime' && topRatio >= 0.6 && secondRatio <= 0.25)
1043
+ return 'runtime';
1044
+ // Testing-heavy dependencies often leak a small runtime footprint (helpers in fixtures).
1045
+ if (top[0] === 'testing' && topRatio >= 0.6 && secondRatio <= 0.3)
1046
+ return 'testing';
1047
+ return 'mixed';
788
1048
  }
789
1049
  function buildLicenseInfo(declaredRaw, licenseText) {
790
1050
  const declaredValue = typeof declaredRaw === 'string' ? declaredRaw.trim() : '';
@@ -880,12 +1140,18 @@ function isFrameworkPackage(name) {
880
1140
  return FRAMEWORK_PACKAGES.has(name);
881
1141
  }
882
1142
  // Heuristic-only classification for why a dependency exists. Kept deterministic and bounded.
883
- function determineIntroduction(direct, rootCauses, runtimeImpact) {
1143
+ function determineIntroduction(direct, scope, rootCauses, runtimeImpact) {
884
1144
  const rootNames = rootCauses.map((root) => root.name);
885
1145
  if (direct)
886
1146
  return 'direct';
887
1147
  if (runtimeImpact === 'testing')
888
1148
  return 'testing';
1149
+ if (runtimeImpact === 'tooling' || runtimeImpact === 'build')
1150
+ return 'tooling';
1151
+ if (scope === 'dev')
1152
+ return 'tooling';
1153
+ if (scope === 'peer' && runtimeImpact !== 'runtime')
1154
+ return 'tooling';
889
1155
  if (rootNames.length > 0 && rootNames.every((root) => isToolingPackage(root)))
890
1156
  return 'tooling';
891
1157
  if (rootNames.some((root) => isFrameworkPackage(root)))
@@ -894,23 +1160,61 @@ function determineIntroduction(direct, rootCauses, runtimeImpact) {
894
1160
  return 'transitive';
895
1161
  return 'unknown';
896
1162
  }
897
- // Upgrade blockers derived only from local metadata (no external lookups).
1163
+ function isPermissiveNodeEngineRange(range) {
1164
+ const compact = range.trim().toLowerCase().replace(/\s+/g, '');
1165
+ return compact === '*' || compact === 'x' || compact === '>=0' || compact === '>=0.0' || compact === '>=0.0.0';
1166
+ }
1167
+ function hasNodeEngineUpgradeBlocker(nodeEngine) {
1168
+ if (!nodeEngine || !nodeEngine.trim())
1169
+ return false;
1170
+ if (isPermissiveNodeEngineRange(nodeEngine))
1171
+ return false;
1172
+ const clauses = nodeEngine.split('||').map((clause) => clause.trim()).filter(Boolean);
1173
+ if (clauses.length > 0 && clauses.every((clause) => isPermissiveNodeEngineRange(clause))) {
1174
+ return false;
1175
+ }
1176
+ const minMajor = parseMinMajorFromRange(nodeEngine);
1177
+ if (minMajor !== undefined) {
1178
+ if (minMajor > 0)
1179
+ return true;
1180
+ return /<\s*\d/.test(nodeEngine);
1181
+ }
1182
+ if (/<\s*\d/.test(nodeEngine))
1183
+ return true;
1184
+ const majors = Array.from(nodeEngine.matchAll(/v?(\d+)/g))
1185
+ .map((match) => Number.parseInt(match[1], 10))
1186
+ .filter((major) => Number.isFinite(major));
1187
+ return majors.some((major) => major > 0);
1188
+ }
1189
+ function hasInstallScriptUpgradeBlocker(execution) {
1190
+ var _a;
1191
+ const hooks = (_a = execution === null || execution === void 0 ? void 0 : execution.scripts) === null || _a === void 0 ? void 0 : _a.hooks;
1192
+ if (!hooks || hooks.length === 0)
1193
+ return false;
1194
+ return hooks.includes('preinstall') || hooks.includes('install') || hooks.includes('postinstall');
1195
+ }
898
1196
  function buildUpgradeBlock(insights) {
899
1197
  var _a;
900
1198
  const blockers = [];
901
- if (insights.nodeEngine)
1199
+ const hasNodeEngineBlocker = hasNodeEngineUpgradeBlocker(insights.nodeEngine);
1200
+ const hasNativeBindingsBlocker = Boolean((_a = insights.execution) === null || _a === void 0 ? void 0 : _a.native);
1201
+ const hasInstallScriptBlocker = hasInstallScriptUpgradeBlocker(insights.execution);
1202
+ if (hasNodeEngineBlocker)
902
1203
  blockers.push('nodeEngine');
903
- if (Object.keys(insights.declaredDependencies.peer).length > 0)
1204
+ if (insights.requiredPeerDependencies > 0)
904
1205
  blockers.push('peerDependency');
905
- if ((_a = insights.execution) === null || _a === void 0 ? void 0 : _a.native)
1206
+ if (hasNativeBindingsBlocker)
906
1207
  blockers.push('nativeBindings');
1208
+ if (hasInstallScriptBlocker)
1209
+ blockers.push('installScripts');
907
1210
  if (insights.deprecated)
908
1211
  blockers.push('deprecated');
909
1212
  if (blockers.length === 0)
910
1213
  return undefined;
1214
+ const blocksNodeMajor = hasNodeEngineBlocker || hasNativeBindingsBlocker || hasInstallScriptBlocker;
911
1215
  return {
912
- blocksNodeMajor: true,
913
- blockers
1216
+ blockers,
1217
+ ...(blocksNodeMajor ? { blocksNodeMajor: true } : {})
914
1218
  };
915
1219
  }
916
1220
  async function gatherPackageInsights(name, version, resolvePaths, metaCache, statCache) {
@@ -920,6 +1224,8 @@ async function gatherPackageInsights(name, version, resolvePaths, metaCache, sta
920
1224
  return {
921
1225
  deprecated: false,
922
1226
  nodeEngine: null,
1227
+ requiredPeerDependencies: 0,
1228
+ hasBin: false,
923
1229
  declaredDependencies: { dep: {}, dev: {}, peer: {}, opt: {} },
924
1230
  tsTypes: 'unknown'
925
1231
  };
@@ -927,18 +1233,21 @@ async function gatherPackageInsights(name, version, resolvePaths, metaCache, sta
927
1233
  const pkg = (meta === null || meta === void 0 ? void 0 : meta.pkg) || {};
928
1234
  const dir = meta === null || meta === void 0 ? void 0 : meta.dir;
929
1235
  const stats = dir ? await calculatePackageStats(dir, statCache) : undefined;
1236
+ const peerDependencies = normalizeDeclaredDeps(pkg.peerDependencies);
930
1237
  const declaredDependencies = {
931
1238
  dep: normalizeDeclaredDeps(pkg.dependencies),
932
1239
  dev: normalizeDeclaredDeps(pkg.devDependencies),
933
- peer: normalizeDeclaredDeps(pkg.peerDependencies),
1240
+ peer: peerDependencies,
934
1241
  opt: normalizeDeclaredDeps(pkg.optionalDependencies)
935
1242
  };
1243
+ const requiredPeerDependencies = countRequiredPeerDependencies(peerDependencies, pkg.peerDependenciesMeta);
936
1244
  const scripts = pkg.scripts || {};
937
1245
  const deprecated = Boolean(pkg.deprecated);
938
1246
  const nodeEngine = typeof ((_a = pkg.engines) === null || _a === void 0 ? void 0 : _a.node) === 'string' ? pkg.engines.node : null;
939
1247
  const description = typeof pkg.description === 'string' && pkg.description.trim()
940
1248
  ? pkg.description.trim()
941
1249
  : undefined;
1250
+ const hasBin = hasPackageBin(pkg.bin);
942
1251
  const hasDefinitelyTyped = await hasDefinitelyTypedPackage(name, resolvePaths, metaCache);
943
1252
  const tsTypes = determineTypes(pkg, (stats === null || stats === void 0 ? void 0 : stats.hasDts) || false, hasDefinitelyTyped);
944
1253
  const links = extractPackageLinks(pkg);
@@ -946,7 +1255,10 @@ async function gatherPackageInsights(name, version, resolvePaths, metaCache, sta
946
1255
  return {
947
1256
  deprecated,
948
1257
  nodeEngine,
1258
+ requiredPeerDependencies,
949
1259
  description,
1260
+ ...(typeof (stats === null || stats === void 0 ? void 0 : stats.fileCount) === 'number' ? { fileCount: stats.fileCount } : {}),
1261
+ hasBin,
950
1262
  declaredDependencies,
951
1263
  links,
952
1264
  execution,
@@ -966,6 +1278,30 @@ function normalizeDeclaredDeps(source) {
966
1278
  }
967
1279
  return out;
968
1280
  }
1281
+ function countRequiredPeerDependencies(peerDependencies, peerDependenciesMeta) {
1282
+ if (!peerDependencies || Object.keys(peerDependencies).length === 0)
1283
+ return 0;
1284
+ const meta = peerDependenciesMeta && typeof peerDependenciesMeta === 'object'
1285
+ ? peerDependenciesMeta
1286
+ : {};
1287
+ let count = 0;
1288
+ for (const peerName of Object.keys(peerDependencies)) {
1289
+ const peerMeta = meta[peerName];
1290
+ const optional = Boolean(peerMeta &&
1291
+ typeof peerMeta === 'object' &&
1292
+ peerMeta.optional === true);
1293
+ if (!optional)
1294
+ count += 1;
1295
+ }
1296
+ return count;
1297
+ }
1298
+ function hasPackageBin(binField) {
1299
+ if (typeof binField === 'string')
1300
+ return binField.trim().length > 0;
1301
+ if (!binField || typeof binField !== 'object')
1302
+ return false;
1303
+ return Object.values(binField).some((value) => typeof value === 'string' && value.trim().length > 0);
1304
+ }
969
1305
  async function loadPackageMeta(name, resolvePaths, cache, version) {
970
1306
  const cacheKey = version ? `${name}@${version}` : name;
971
1307
  if (cache.has(cacheKey))
@@ -1010,6 +1346,7 @@ async function calculatePackageStats(dir, cache) {
1010
1346
  let hasDts = false;
1011
1347
  let hasNativeBinary = false;
1012
1348
  let hasBindingGyp = false;
1349
+ let fileCount = 0;
1013
1350
  async function walk(current) {
1014
1351
  const entries = await promises_1.default.readdir(current, { withFileTypes: true });
1015
1352
  for (const entry of entries) {
@@ -1017,9 +1354,13 @@ async function calculatePackageStats(dir, cache) {
1017
1354
  if (entry.isSymbolicLink())
1018
1355
  continue;
1019
1356
  if (entry.isDirectory()) {
1357
+ // Ignore nested dependency stores to keep package-level stats bounded and comparable.
1358
+ if (entry.name === 'node_modules' || entry.name === '.git')
1359
+ continue;
1020
1360
  await walk(full);
1021
1361
  }
1022
1362
  else if (entry.isFile()) {
1363
+ fileCount += 1;
1023
1364
  if (entry.name.endsWith('.d.ts'))
1024
1365
  hasDts = true;
1025
1366
  if (entry.name.endsWith('.node'))
@@ -1035,7 +1376,7 @@ async function calculatePackageStats(dir, cache) {
1035
1376
  catch (err) {
1036
1377
  // best-effort; ignore inaccessible paths
1037
1378
  }
1038
- const result = { hasDts, hasNativeBinary, hasBindingGyp };
1379
+ const result = { hasDts, hasNativeBinary, hasBindingGyp, fileCount };
1039
1380
  cache.set(dir, result);
1040
1381
  return result;
1041
1382
  }