@getcodesentinel/codesentinel 1.4.2 → 1.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 +10 -0
- package/dist/index.js +1827 -1124
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,1157 +2,981 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command, Option } from "commander";
|
|
5
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
6
|
-
import { dirname, resolve as resolve3 } from "path";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
5
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
external: summary.external.available ? {
|
|
22
|
-
available: true,
|
|
23
|
-
metrics: summary.external.metrics,
|
|
24
|
-
highRiskDependenciesTop: summary.external.highRiskDependencies.slice(0, 10)
|
|
25
|
-
} : {
|
|
26
|
-
available: false,
|
|
27
|
-
reason: summary.external.reason
|
|
28
|
-
},
|
|
29
|
-
risk: {
|
|
30
|
-
repositoryScore: summary.risk.repositoryScore,
|
|
31
|
-
normalizedScore: summary.risk.normalizedScore,
|
|
32
|
-
hotspotsTop: summary.risk.hotspots.slice(0, 5).map((hotspot) => ({
|
|
33
|
-
file: hotspot.file,
|
|
34
|
-
score: hotspot.score
|
|
35
|
-
})),
|
|
36
|
-
fragileClusterCount: summary.risk.fragileClusters.length,
|
|
37
|
-
dependencyAmplificationZoneCount: summary.risk.dependencyAmplificationZones.length
|
|
6
|
+
// ../dependency-firewall/dist/index.js
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
10
|
+
var round4 = (value) => Number(value.toFixed(4));
|
|
11
|
+
var normalizeNodes = (nodes) => {
|
|
12
|
+
const byName = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const node of nodes) {
|
|
14
|
+
const bucket = byName.get(node.name) ?? [];
|
|
15
|
+
bucket.push(node);
|
|
16
|
+
byName.set(node.name, bucket);
|
|
38
17
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return false;
|
|
18
|
+
const normalized = [];
|
|
19
|
+
for (const [name, candidates] of byName.entries()) {
|
|
20
|
+
if (candidates.length === 0) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
candidates.sort((a, b) => b.version.localeCompare(a.version));
|
|
24
|
+
const selected = candidates[0];
|
|
25
|
+
if (selected === void 0) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const deps = selected.dependencies.map((dep) => {
|
|
29
|
+
const at = dep.lastIndexOf("@");
|
|
30
|
+
return at <= 0 ? dep : dep.slice(0, at);
|
|
31
|
+
}).filter((depName) => depName.length > 0).sort((a, b) => a.localeCompare(b));
|
|
32
|
+
normalized.push({
|
|
33
|
+
key: `${name}@${selected.version}`,
|
|
34
|
+
name,
|
|
35
|
+
version: selected.version,
|
|
36
|
+
dependencies: deps
|
|
37
|
+
});
|
|
60
38
|
}
|
|
61
|
-
return
|
|
62
|
-
};
|
|
63
|
-
var write = (messageLevel, message) => {
|
|
64
|
-
process.stderr.write(`[codesentinel] ${messageLevel.toUpperCase()} ${message}
|
|
65
|
-
`);
|
|
39
|
+
return normalized.sort((a, b) => a.name.localeCompare(b.name));
|
|
66
40
|
};
|
|
67
|
-
var
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
41
|
+
var computeDepths = (nodeByName, directNames) => {
|
|
42
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
43
|
+
const depthByName = /* @__PURE__ */ new Map();
|
|
44
|
+
const compute = (name) => {
|
|
45
|
+
const known = depthByName.get(name);
|
|
46
|
+
if (known !== void 0) {
|
|
47
|
+
return known;
|
|
48
|
+
}
|
|
49
|
+
if (visiting.has(name)) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
visiting.add(name);
|
|
53
|
+
const node = nodeByName.get(name);
|
|
54
|
+
if (node === void 0) {
|
|
55
|
+
visiting.delete(name);
|
|
56
|
+
depthByName.set(name, 0);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
let maxChildDepth = 0;
|
|
60
|
+
for (const dependencyName of node.dependencies) {
|
|
61
|
+
const childDepth = compute(dependencyName);
|
|
62
|
+
if (childDepth > maxChildDepth) {
|
|
63
|
+
maxChildDepth = childDepth;
|
|
90
64
|
}
|
|
91
65
|
}
|
|
66
|
+
visiting.delete(name);
|
|
67
|
+
const ownDepth = directNames.has(name) ? 0 : maxChildDepth + 1;
|
|
68
|
+
depthByName.set(name, ownDepth);
|
|
69
|
+
return ownDepth;
|
|
92
70
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return value;
|
|
102
|
-
default:
|
|
103
|
-
return "info";
|
|
71
|
+
for (const name of nodeByName.keys()) {
|
|
72
|
+
compute(name);
|
|
73
|
+
}
|
|
74
|
+
let maxDepth = 0;
|
|
75
|
+
for (const depth of depthByName.values()) {
|
|
76
|
+
if (depth > maxDepth) {
|
|
77
|
+
maxDepth = depth;
|
|
78
|
+
}
|
|
104
79
|
}
|
|
80
|
+
return { depthByName, maxDepth };
|
|
105
81
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
var
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
82
|
+
var rankCentrality = (nodes, dependentsByName, directNames, topN) => [...nodes].map((node) => ({
|
|
83
|
+
name: node.name,
|
|
84
|
+
dependents: dependentsByName.get(node.name) ?? 0,
|
|
85
|
+
fanOut: node.dependencies.length,
|
|
86
|
+
direct: directNames.has(node.name)
|
|
87
|
+
})).sort(
|
|
88
|
+
(a, b) => b.dependents - a.dependents || b.fanOut - a.fanOut || a.name.localeCompare(b.name)
|
|
89
|
+
).slice(0, topN);
|
|
90
|
+
var canPropagateSignal = (signal) => signal === "abandoned" || signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout";
|
|
91
|
+
var collectTransitiveDependencies = (rootName, nodeByName) => {
|
|
92
|
+
const seen = /* @__PURE__ */ new Set();
|
|
93
|
+
const stack = [...nodeByName.get(rootName)?.dependencies ?? []];
|
|
94
|
+
while (stack.length > 0) {
|
|
95
|
+
const current = stack.pop();
|
|
96
|
+
if (current === void 0 || seen.has(current) || current === rootName) {
|
|
120
97
|
continue;
|
|
121
98
|
}
|
|
122
|
-
|
|
99
|
+
seen.add(current);
|
|
100
|
+
const currentNode = nodeByName.get(current);
|
|
101
|
+
if (currentNode === void 0) {
|
|
123
102
|
continue;
|
|
124
103
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (fromCompare !== 0) {
|
|
130
|
-
return fromCompare;
|
|
104
|
+
for (const next of currentNode.dependencies) {
|
|
105
|
+
if (!seen.has(next)) {
|
|
106
|
+
stack.push(next);
|
|
107
|
+
}
|
|
131
108
|
}
|
|
132
|
-
return a.to.localeCompare(b.to);
|
|
133
|
-
});
|
|
134
|
-
const adjacency = /* @__PURE__ */ new Map();
|
|
135
|
-
for (const node of sortedNodes) {
|
|
136
|
-
adjacency.set(node.id, []);
|
|
137
|
-
}
|
|
138
|
-
for (const edge of sortedEdges) {
|
|
139
|
-
adjacency.get(edge.from)?.push(edge.to);
|
|
140
|
-
}
|
|
141
|
-
const adjacencyById = /* @__PURE__ */ new Map();
|
|
142
|
-
for (const [nodeId, targets] of adjacency.entries()) {
|
|
143
|
-
adjacencyById.set(nodeId, [...targets]);
|
|
144
109
|
}
|
|
145
|
-
return
|
|
146
|
-
nodes: sortedNodes,
|
|
147
|
-
edges: sortedEdges,
|
|
148
|
-
adjacencyById
|
|
149
|
-
};
|
|
110
|
+
return [...seen].sort((a, b) => a.localeCompare(b));
|
|
150
111
|
};
|
|
151
|
-
var
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
onStack.add(nodeId);
|
|
164
|
-
const neighbors = adjacencyById.get(nodeId) ?? [];
|
|
165
|
-
for (const nextId of neighbors) {
|
|
166
|
-
if (!indices.has(nextId)) {
|
|
167
|
-
strongConnect(nextId);
|
|
168
|
-
const nodeLowLink2 = lowLink.get(nodeId);
|
|
169
|
-
const nextLowLink = lowLink.get(nextId);
|
|
170
|
-
if (nodeLowLink2 !== void 0 && nextLowLink !== void 0 && nextLowLink < nodeLowLink2) {
|
|
171
|
-
lowLink.set(nodeId, nextLowLink);
|
|
172
|
-
}
|
|
112
|
+
var buildExternalAnalysisSummary = (targetPath, extraction, metadataByKey, config) => {
|
|
113
|
+
const nodes = normalizeNodes(extraction.nodes);
|
|
114
|
+
const directNames = new Set(extraction.directDependencies.map((dep) => dep.name));
|
|
115
|
+
const directSpecByName = new Map(extraction.directDependencies.map((dep) => [dep.name, dep]));
|
|
116
|
+
const nodeByName = new Map(nodes.map((node) => [node.name, node]));
|
|
117
|
+
const dependentsByName = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const node of nodes) {
|
|
119
|
+
dependentsByName.set(node.name, dependentsByName.get(node.name) ?? 0);
|
|
120
|
+
}
|
|
121
|
+
for (const node of nodes) {
|
|
122
|
+
for (const dependencyName of node.dependencies) {
|
|
123
|
+
if (!nodeByName.has(dependencyName)) {
|
|
173
124
|
continue;
|
|
174
125
|
}
|
|
175
|
-
|
|
176
|
-
const nodeLowLink2 = lowLink.get(nodeId);
|
|
177
|
-
const nextIndex = indices.get(nextId);
|
|
178
|
-
if (nodeLowLink2 !== void 0 && nextIndex !== void 0 && nextIndex < nodeLowLink2) {
|
|
179
|
-
lowLink.set(nodeId, nextIndex);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const nodeLowLink = lowLink.get(nodeId);
|
|
184
|
-
const nodeIndex = indices.get(nodeId);
|
|
185
|
-
if (nodeLowLink === void 0 || nodeIndex === void 0 || nodeLowLink !== nodeIndex) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const component = [];
|
|
189
|
-
for (; ; ) {
|
|
190
|
-
const popped = stack.pop();
|
|
191
|
-
if (popped === void 0) {
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
onStack.delete(popped);
|
|
195
|
-
component.push(popped);
|
|
196
|
-
if (popped === nodeId) {
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
component.sort((a, b) => a.localeCompare(b));
|
|
201
|
-
components.push(component);
|
|
202
|
-
};
|
|
203
|
-
const nodeIds = [...adjacencyById.keys()].sort((a, b) => a.localeCompare(b));
|
|
204
|
-
for (const nodeId of nodeIds) {
|
|
205
|
-
if (!indices.has(nodeId)) {
|
|
206
|
-
strongConnect(nodeId);
|
|
126
|
+
dependentsByName.set(dependencyName, (dependentsByName.get(dependencyName) ?? 0) + 1);
|
|
207
127
|
}
|
|
208
128
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
var computeCyclesAndDepth = (graph) => {
|
|
221
|
-
const { components } = runTarjanScc(graph.adjacencyById);
|
|
222
|
-
const cycles = [];
|
|
223
|
-
const componentByNodeId = /* @__PURE__ */ new Map();
|
|
224
|
-
components.forEach((component, index) => {
|
|
225
|
-
for (const nodeId of component) {
|
|
226
|
-
componentByNodeId.set(nodeId, index);
|
|
129
|
+
const { depthByName, maxDepth } = computeDepths(nodeByName, directNames);
|
|
130
|
+
const centralityRanking = rankCentrality(nodes, dependentsByName, directNames, config.centralityTopN);
|
|
131
|
+
const topCentralNames = new Set(
|
|
132
|
+
centralityRanking.slice(0, Math.max(1, Math.ceil(centralityRanking.length * 0.25))).map((entry) => entry.name)
|
|
133
|
+
);
|
|
134
|
+
const allDependencies = [];
|
|
135
|
+
let metadataAvailableCount = 0;
|
|
136
|
+
for (const node of nodes) {
|
|
137
|
+
const metadata = metadataByKey.get(node.key) ?? null;
|
|
138
|
+
if (metadata !== null) {
|
|
139
|
+
metadataAvailableCount += 1;
|
|
227
140
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
141
|
+
const dependencyDepth = depthByName.get(node.name) ?? 0;
|
|
142
|
+
const dependents = dependentsByName.get(node.name) ?? 0;
|
|
143
|
+
const riskSignals = [];
|
|
144
|
+
if ((metadata?.maintainerCount ?? 0) === 1) {
|
|
145
|
+
riskSignals.push("single_maintainer");
|
|
231
146
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
cycles.push({ nodes: [...component] });
|
|
147
|
+
if ((metadata?.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold) {
|
|
148
|
+
riskSignals.push("abandoned");
|
|
235
149
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
239
|
-
for (let i = 0; i < components.length; i += 1) {
|
|
240
|
-
dagOutgoing.set(i, /* @__PURE__ */ new Set());
|
|
241
|
-
inDegree.set(i, 0);
|
|
242
|
-
}
|
|
243
|
-
for (const edge of graph.edges) {
|
|
244
|
-
const fromComponent = componentByNodeId.get(edge.from);
|
|
245
|
-
const toComponent = componentByNodeId.get(edge.to);
|
|
246
|
-
if (fromComponent === void 0 || toComponent === void 0 || fromComponent === toComponent) {
|
|
247
|
-
continue;
|
|
150
|
+
if (topCentralNames.has(node.name) && dependents > 0) {
|
|
151
|
+
riskSignals.push("high_centrality");
|
|
248
152
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
continue;
|
|
153
|
+
if (dependencyDepth >= config.deepChainThreshold) {
|
|
154
|
+
riskSignals.push("deep_chain");
|
|
252
155
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
const queue = [];
|
|
257
|
-
const depthByComponent = /* @__PURE__ */ new Map();
|
|
258
|
-
for (let i = 0; i < components.length; i += 1) {
|
|
259
|
-
if ((inDegree.get(i) ?? 0) === 0) {
|
|
260
|
-
queue.push(i);
|
|
261
|
-
depthByComponent.set(i, 0);
|
|
156
|
+
if (node.dependencies.length >= config.fanOutHighThreshold) {
|
|
157
|
+
riskSignals.push("high_fanout");
|
|
262
158
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
while (cursor < queue.length) {
|
|
266
|
-
const componentId = queue[cursor];
|
|
267
|
-
cursor += 1;
|
|
268
|
-
if (componentId === void 0) {
|
|
269
|
-
continue;
|
|
159
|
+
if (metadata === null) {
|
|
160
|
+
riskSignals.push("metadata_unavailable");
|
|
270
161
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
162
|
+
allDependencies.push({
|
|
163
|
+
name: node.name,
|
|
164
|
+
direct: directNames.has(node.name),
|
|
165
|
+
dependencyScope: directSpecByName.get(node.name)?.scope ?? "prod",
|
|
166
|
+
requestedRange: directSpecByName.get(node.name)?.requestedRange ?? null,
|
|
167
|
+
resolvedVersion: node.version,
|
|
168
|
+
transitiveDependencies: [],
|
|
169
|
+
weeklyDownloads: metadata?.weeklyDownloads ?? null,
|
|
170
|
+
dependencyDepth,
|
|
171
|
+
fanOut: node.dependencies.length,
|
|
172
|
+
dependents,
|
|
173
|
+
maintainerCount: metadata?.maintainerCount ?? null,
|
|
174
|
+
releaseFrequencyDays: metadata?.releaseFrequencyDays ?? null,
|
|
175
|
+
daysSinceLastRelease: metadata?.daysSinceLastRelease ?? null,
|
|
176
|
+
repositoryActivity30d: metadata?.repositoryActivity30d ?? null,
|
|
177
|
+
busFactor: metadata?.busFactor ?? null,
|
|
178
|
+
ownRiskSignals: [...riskSignals].sort((a, b) => a.localeCompare(b)),
|
|
179
|
+
inheritedRiskSignals: [],
|
|
180
|
+
riskSignals
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
allDependencies.sort((a, b) => a.name.localeCompare(b.name));
|
|
184
|
+
const allByName = new Map(allDependencies.map((dep) => [dep.name, dep]));
|
|
185
|
+
const dependencies = allDependencies.filter((dep) => dep.direct).map((dep) => {
|
|
186
|
+
const transitiveDependencies = collectTransitiveDependencies(dep.name, nodeByName);
|
|
187
|
+
const inheritedSignals = /* @__PURE__ */ new Set();
|
|
188
|
+
const allSignals = new Set(dep.ownRiskSignals);
|
|
189
|
+
for (const transitiveName of transitiveDependencies) {
|
|
190
|
+
const transitive = allByName.get(transitiveName);
|
|
191
|
+
if (transitive === void 0) {
|
|
192
|
+
continue;
|
|
277
193
|
}
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
194
|
+
for (const signal of transitive.riskSignals) {
|
|
195
|
+
if (canPropagateSignal(signal)) {
|
|
196
|
+
inheritedSignals.add(signal);
|
|
197
|
+
allSignals.add(signal);
|
|
198
|
+
}
|
|
282
199
|
}
|
|
283
200
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
201
|
+
return {
|
|
202
|
+
...dep,
|
|
203
|
+
transitiveDependencies,
|
|
204
|
+
inheritedRiskSignals: [...inheritedSignals].sort((a, b) => a.localeCompare(b)),
|
|
205
|
+
riskSignals: [...allSignals].sort((a, b) => a.localeCompare(b))
|
|
206
|
+
};
|
|
207
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
208
|
+
const highRiskDependencies = dependencies.filter(
|
|
209
|
+
(dep) => dep.ownRiskSignals.includes("abandoned") || dep.ownRiskSignals.filter(
|
|
210
|
+
(signal) => signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout"
|
|
211
|
+
).length >= 2 || dep.ownRiskSignals.includes("single_maintainer") && ((dep.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold / 2 || (dep.repositoryActivity30d ?? 1) <= 0)
|
|
212
|
+
).filter((dep) => dep.dependencyScope === "prod").sort(
|
|
213
|
+
(a, b) => b.ownRiskSignals.length - a.ownRiskSignals.length || a.name.localeCompare(b.name)
|
|
214
|
+
).slice(0, config.maxHighRiskDependencies).map((dep) => dep.name);
|
|
215
|
+
const highRiskDevelopmentDependencies = dependencies.filter(
|
|
216
|
+
(dep) => dep.dependencyScope === "dev" && (dep.ownRiskSignals.includes("abandoned") || dep.ownRiskSignals.filter(
|
|
217
|
+
(signal) => signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout"
|
|
218
|
+
).length >= 2 || dep.ownRiskSignals.includes("single_maintainer") && ((dep.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold / 2 || (dep.repositoryActivity30d ?? 1) <= 0))
|
|
219
|
+
).sort(
|
|
220
|
+
(a, b) => b.ownRiskSignals.length - a.ownRiskSignals.length || a.name.localeCompare(b.name)
|
|
221
|
+
).slice(0, config.maxHighRiskDependencies).map((dep) => dep.name);
|
|
222
|
+
const transitiveExposureDependencies = dependencies.filter((dep) => dep.inheritedRiskSignals.length > 0).sort(
|
|
223
|
+
(a, b) => b.inheritedRiskSignals.length - a.inheritedRiskSignals.length || a.name.localeCompare(b.name)
|
|
224
|
+
).map((dep) => dep.name);
|
|
225
|
+
const singleMaintainerDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("single_maintainer")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
226
|
+
const abandonedDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("abandoned")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
301
227
|
return {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
228
|
+
targetPath,
|
|
229
|
+
available: true,
|
|
230
|
+
metrics: {
|
|
231
|
+
totalDependencies: allDependencies.length,
|
|
232
|
+
directDependencies: dependencies.length,
|
|
233
|
+
directProductionDependencies: dependencies.filter((dependency) => dependency.dependencyScope === "prod").length,
|
|
234
|
+
directDevelopmentDependencies: dependencies.filter((dependency) => dependency.dependencyScope === "dev").length,
|
|
235
|
+
transitiveDependencies: allDependencies.length - dependencies.length,
|
|
236
|
+
dependencyDepth: maxDepth,
|
|
237
|
+
lockfileKind: extraction.kind,
|
|
238
|
+
metadataCoverage: allDependencies.length === 0 ? 0 : round4(metadataAvailableCount / allDependencies.length)
|
|
239
|
+
},
|
|
240
|
+
dependencies,
|
|
241
|
+
highRiskDependencies,
|
|
242
|
+
highRiskDevelopmentDependencies,
|
|
243
|
+
transitiveExposureDependencies,
|
|
244
|
+
singleMaintainerDependencies,
|
|
245
|
+
abandonedDependencies,
|
|
246
|
+
centralityRanking
|
|
305
247
|
};
|
|
306
248
|
};
|
|
307
|
-
var
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
249
|
+
var DEFAULT_EXTERNAL_ANALYSIS_CONFIG = {
|
|
250
|
+
abandonedDaysThreshold: 540,
|
|
251
|
+
deepChainThreshold: 6,
|
|
252
|
+
fanOutHighThreshold: 25,
|
|
253
|
+
centralityTopN: 20,
|
|
254
|
+
maxHighRiskDependencies: 100,
|
|
255
|
+
metadataRequestConcurrency: 8
|
|
256
|
+
};
|
|
257
|
+
var LOCKFILE_CANDIDATES = [
|
|
258
|
+
{ fileName: "pnpm-lock.yaml", kind: "pnpm" },
|
|
259
|
+
{ fileName: "package-lock.json", kind: "npm" },
|
|
260
|
+
{ fileName: "npm-shrinkwrap.json", kind: "npm-shrinkwrap" },
|
|
261
|
+
{ fileName: "yarn.lock", kind: "yarn" },
|
|
262
|
+
{ fileName: "bun.lock", kind: "bun" },
|
|
263
|
+
{ fileName: "bun.lockb", kind: "bun" }
|
|
264
|
+
];
|
|
265
|
+
var loadPackageJson = (repositoryPath) => {
|
|
266
|
+
const packageJsonPath2 = join(repositoryPath, "package.json");
|
|
267
|
+
if (!existsSync(packageJsonPath2)) {
|
|
268
|
+
return null;
|
|
316
269
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
maxFanOut = fanOut;
|
|
270
|
+
return {
|
|
271
|
+
path: packageJsonPath2,
|
|
272
|
+
raw: readFileSync(packageJsonPath2, "utf8")
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
var selectLockfile = (repositoryPath) => {
|
|
276
|
+
for (const candidate of LOCKFILE_CANDIDATES) {
|
|
277
|
+
const absolutePath = join(repositoryPath, candidate.fileName);
|
|
278
|
+
if (!existsSync(absolutePath)) {
|
|
279
|
+
continue;
|
|
328
280
|
}
|
|
329
281
|
return {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
fanIn,
|
|
334
|
-
fanOut,
|
|
335
|
-
depth: depthByNodeId.get(node.id) ?? 0
|
|
282
|
+
path: absolutePath,
|
|
283
|
+
kind: candidate.kind,
|
|
284
|
+
raw: readFileSync(absolutePath, "utf8")
|
|
336
285
|
};
|
|
337
|
-
});
|
|
338
|
-
const metrics = {
|
|
339
|
-
nodeCount: graph.nodes.length,
|
|
340
|
-
edgeCount: graph.edges.length,
|
|
341
|
-
cycleCount: cycles.length,
|
|
342
|
-
graphDepth,
|
|
343
|
-
maxFanIn,
|
|
344
|
-
maxFanOut
|
|
345
|
-
};
|
|
346
|
-
return {
|
|
347
|
-
targetPath,
|
|
348
|
-
nodes: graph.nodes,
|
|
349
|
-
edges: graph.edges,
|
|
350
|
-
cycles,
|
|
351
|
-
files,
|
|
352
|
-
metrics
|
|
353
|
-
};
|
|
354
|
-
};
|
|
355
|
-
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
356
|
-
var SCAN_EXCLUDES = [
|
|
357
|
-
"**/node_modules/**",
|
|
358
|
-
"**/.git/**",
|
|
359
|
-
"**/dist/**",
|
|
360
|
-
"**/build/**",
|
|
361
|
-
"**/.next/**",
|
|
362
|
-
"**/coverage/**",
|
|
363
|
-
"**/.turbo/**",
|
|
364
|
-
"**/.cache/**",
|
|
365
|
-
"**/out/**"
|
|
366
|
-
];
|
|
367
|
-
var SCAN_INCLUDES = ["**/*"];
|
|
368
|
-
var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
369
|
-
"node_modules",
|
|
370
|
-
".git",
|
|
371
|
-
"dist",
|
|
372
|
-
"build",
|
|
373
|
-
".next",
|
|
374
|
-
"coverage",
|
|
375
|
-
".turbo",
|
|
376
|
-
".cache",
|
|
377
|
-
"out"
|
|
378
|
-
]);
|
|
379
|
-
var normalizePath = (pathValue) => pathValue.replaceAll("\\", "/");
|
|
380
|
-
var isProjectSourceFile = (filePath, projectRoot) => {
|
|
381
|
-
const extension = extname(filePath);
|
|
382
|
-
if (!SOURCE_EXTENSIONS.has(extension)) {
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
const relativePath = relative(projectRoot, filePath);
|
|
386
|
-
if (relativePath.startsWith("..")) {
|
|
387
|
-
return false;
|
|
388
286
|
}
|
|
389
|
-
|
|
390
|
-
const segments = normalizedRelativePath.split("/");
|
|
391
|
-
return !segments.some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
287
|
+
return null;
|
|
392
288
|
};
|
|
393
|
-
var
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
289
|
+
var parsePackageJson = (raw) => {
|
|
290
|
+
const parsed = JSON.parse(raw);
|
|
291
|
+
const merged = /* @__PURE__ */ new Map();
|
|
292
|
+
const addBlock = (block, scope) => {
|
|
293
|
+
if (block === void 0) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
for (const [name, versionRange] of Object.entries(block)) {
|
|
297
|
+
const existing = merged.get(name);
|
|
298
|
+
if (existing?.scope === "prod" && scope === "dev") {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
merged.set(name, { name, requestedRange: versionRange, scope });
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
addBlock(parsed.dependencies, "prod");
|
|
305
|
+
addBlock(parsed.optionalDependencies, "prod");
|
|
306
|
+
addBlock(parsed.peerDependencies, "prod");
|
|
307
|
+
addBlock(parsed.devDependencies, "dev");
|
|
308
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
401
309
|
};
|
|
402
|
-
var
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
{
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
310
|
+
var parsePackageLock = (raw, directSpecs) => {
|
|
311
|
+
const parsed = JSON.parse(raw);
|
|
312
|
+
const nodes = [];
|
|
313
|
+
if (parsed.packages !== void 0) {
|
|
314
|
+
for (const [packagePath, packageData] of Object.entries(parsed.packages)) {
|
|
315
|
+
if (packagePath.length === 0 || packageData.version === void 0) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const segments = packagePath.split("node_modules/");
|
|
319
|
+
const name = segments[segments.length - 1] ?? "";
|
|
320
|
+
if (name.length === 0) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const dependencies = Object.entries(packageData.dependencies ?? {}).map(([depName, depRange]) => `${depName}@${String(depRange)}`).sort((a, b) => a.localeCompare(b));
|
|
324
|
+
nodes.push({
|
|
325
|
+
name,
|
|
326
|
+
version: packageData.version,
|
|
327
|
+
dependencies
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
} else if (parsed.dependencies !== void 0) {
|
|
331
|
+
for (const [name, dep] of Object.entries(parsed.dependencies)) {
|
|
332
|
+
if (dep.version === void 0) {
|
|
333
|
+
continue;
|
|
410
334
|
}
|
|
335
|
+
const dependencies = Object.entries(dep.dependencies ?? {}).map(([depName, depVersion]) => `${depName}@${String(depVersion)}`).sort((a, b) => a.localeCompare(b));
|
|
336
|
+
nodes.push({
|
|
337
|
+
name,
|
|
338
|
+
version: dep.version,
|
|
339
|
+
dependencies
|
|
340
|
+
});
|
|
411
341
|
}
|
|
412
|
-
);
|
|
413
|
-
if (parsedCommandLine === void 0) {
|
|
414
|
-
throw new Error(`Failed to parse TypeScript configuration at ${configPath}`);
|
|
415
342
|
}
|
|
416
|
-
|
|
343
|
+
nodes.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
344
|
+
return {
|
|
345
|
+
kind: "npm",
|
|
346
|
+
directDependencies: directSpecs,
|
|
347
|
+
nodes
|
|
348
|
+
};
|
|
417
349
|
};
|
|
418
|
-
var
|
|
419
|
-
|
|
420
|
-
|
|
350
|
+
var sanitizeValue = (value) => value.replace(/^['"]|['"]$/g, "").trim();
|
|
351
|
+
var parsePackageKey = (rawKey) => {
|
|
352
|
+
const key = sanitizeValue(rawKey.replace(/:$/, ""));
|
|
353
|
+
const withoutSlash = key.startsWith("/") ? key.slice(1) : key;
|
|
354
|
+
const lastAt = withoutSlash.lastIndexOf("@");
|
|
355
|
+
if (lastAt <= 0) {
|
|
421
356
|
return null;
|
|
422
357
|
}
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
358
|
+
const name = withoutSlash.slice(0, lastAt);
|
|
359
|
+
const versionWithPeers = withoutSlash.slice(lastAt + 1);
|
|
360
|
+
const version2 = versionWithPeers.split("(")[0] ?? versionWithPeers;
|
|
361
|
+
if (name.length === 0 || version2.length === 0) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return { name, version: version2 };
|
|
365
|
+
};
|
|
366
|
+
var parsePnpmLockfile = (raw, directSpecs) => {
|
|
367
|
+
const lines = raw.split("\n");
|
|
368
|
+
let state = "root";
|
|
369
|
+
let currentPackage = null;
|
|
370
|
+
let currentDependencyName = null;
|
|
371
|
+
const dependenciesByNode = /* @__PURE__ */ new Map();
|
|
372
|
+
for (const line of lines) {
|
|
373
|
+
if (line.trim().length === 0 || line.trimStart().startsWith("#")) {
|
|
374
|
+
continue;
|
|
430
375
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
rootOptions = parsed.options;
|
|
376
|
+
if (line.startsWith("importers:")) {
|
|
377
|
+
state = "importers";
|
|
378
|
+
continue;
|
|
435
379
|
}
|
|
436
|
-
|
|
437
|
-
|
|
380
|
+
if (line.startsWith("packages:")) {
|
|
381
|
+
state = "packages";
|
|
382
|
+
continue;
|
|
438
383
|
}
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
384
|
+
if (state === "packages" || state === "packageDeps") {
|
|
385
|
+
const packageMatch = line.match(/^\s{2}([^\s].+):\s*$/);
|
|
386
|
+
if (packageMatch !== null) {
|
|
387
|
+
const parsedKey = parsePackageKey(packageMatch[1] ?? "");
|
|
388
|
+
if (parsedKey !== null) {
|
|
389
|
+
currentPackage = `${parsedKey.name}@${parsedKey.version}`;
|
|
390
|
+
dependenciesByNode.set(currentPackage, /* @__PURE__ */ new Set());
|
|
391
|
+
state = "packageDeps";
|
|
392
|
+
currentDependencyName = null;
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (state === "packageDeps" && currentPackage !== null) {
|
|
398
|
+
const depLine = line.match(/^\s{6}([^:\s]+):\s*(.+)$/);
|
|
399
|
+
if (depLine !== null) {
|
|
400
|
+
const depName = sanitizeValue(depLine[1] ?? "");
|
|
401
|
+
const depRef = sanitizeValue(depLine[2] ?? "");
|
|
402
|
+
const depVersion = depRef.split("(")[0] ?? depRef;
|
|
403
|
+
if (depName.length > 0 && depVersion.length > 0) {
|
|
404
|
+
dependenciesByNode.get(currentPackage)?.add(`${depName}@${depVersion}`);
|
|
405
|
+
}
|
|
406
|
+
currentDependencyName = null;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const depBlockLine = line.match(/^\s{6}([^:\s]+):\s*$/);
|
|
410
|
+
if (depBlockLine !== null) {
|
|
411
|
+
currentDependencyName = sanitizeValue(depBlockLine[1] ?? "");
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const depVersionLine = line.match(/^\s{8}version:\s*(.+)$/);
|
|
415
|
+
if (depVersionLine !== null && currentDependencyName !== null) {
|
|
416
|
+
const depRef = sanitizeValue(depVersionLine[1] ?? "");
|
|
417
|
+
const depVersion = depRef.split("(")[0] ?? depRef;
|
|
418
|
+
if (depVersion.length > 0) {
|
|
419
|
+
dependenciesByNode.get(currentPackage)?.add(`${currentDependencyName}@${depVersion}`);
|
|
420
|
+
}
|
|
421
|
+
currentDependencyName = null;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (line.match(/^\s{4}(dependencies|optionalDependencies):\s*$/) !== null) {
|
|
425
|
+
continue;
|
|
444
426
|
}
|
|
445
427
|
}
|
|
446
|
-
};
|
|
447
|
-
visitConfig(rootConfigPath);
|
|
448
|
-
return {
|
|
449
|
-
fileNames: [...collectedFiles],
|
|
450
|
-
rootOptions: rootOptions ?? {
|
|
451
|
-
moduleResolution: ts.ModuleResolutionKind.NodeNext
|
|
452
|
-
},
|
|
453
|
-
visitedConfigCount: visitedConfigPaths.size
|
|
454
|
-
};
|
|
455
|
-
};
|
|
456
|
-
var createCompilerOptions = (base) => ({
|
|
457
|
-
...base,
|
|
458
|
-
allowJs: true,
|
|
459
|
-
moduleResolution: base?.moduleResolution ?? ts.ModuleResolutionKind.NodeNext
|
|
460
|
-
});
|
|
461
|
-
var parseTsConfig = (projectRoot) => {
|
|
462
|
-
const collected = collectFilesFromTsConfigGraph(projectRoot);
|
|
463
|
-
if (collected === null) {
|
|
464
|
-
return {
|
|
465
|
-
fileNames: discoverSourceFilesByScan(projectRoot),
|
|
466
|
-
options: createCompilerOptions(void 0),
|
|
467
|
-
tsconfigCount: 0,
|
|
468
|
-
usedFallbackScan: true
|
|
469
|
-
};
|
|
470
428
|
}
|
|
471
|
-
|
|
429
|
+
const nodes = [...dependenciesByNode.entries()].map(([nodeId, deps]) => {
|
|
430
|
+
const at = nodeId.lastIndexOf("@");
|
|
472
431
|
return {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
usedFallbackScan: true
|
|
432
|
+
name: nodeId.slice(0, at),
|
|
433
|
+
version: nodeId.slice(at + 1),
|
|
434
|
+
dependencies: [...deps].sort((a, b) => a.localeCompare(b))
|
|
477
435
|
};
|
|
478
|
-
}
|
|
436
|
+
}).sort(
|
|
437
|
+
(a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version)
|
|
438
|
+
);
|
|
479
439
|
return {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
usedFallbackScan: false
|
|
440
|
+
kind: "pnpm",
|
|
441
|
+
directDependencies: directSpecs,
|
|
442
|
+
nodes
|
|
484
443
|
};
|
|
485
444
|
};
|
|
486
|
-
var
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return expression.text;
|
|
492
|
-
}
|
|
493
|
-
return void 0;
|
|
494
|
-
};
|
|
495
|
-
var hasRuntimeImport = (importDeclaration) => {
|
|
496
|
-
const importClause = importDeclaration.importClause;
|
|
497
|
-
if (importClause === void 0) {
|
|
498
|
-
return true;
|
|
499
|
-
}
|
|
500
|
-
if (importClause.isTypeOnly) {
|
|
501
|
-
return false;
|
|
502
|
-
}
|
|
503
|
-
if (importClause.name !== void 0) {
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
const namedBindings = importClause.namedBindings;
|
|
507
|
-
if (namedBindings === void 0) {
|
|
508
|
-
return false;
|
|
509
|
-
}
|
|
510
|
-
if (ts.isNamespaceImport(namedBindings)) {
|
|
511
|
-
return true;
|
|
445
|
+
var stripQuotes = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
446
|
+
var parseVersionSelector = (selector) => {
|
|
447
|
+
const npmIndex = selector.lastIndexOf("@npm:");
|
|
448
|
+
if (npmIndex >= 0) {
|
|
449
|
+
return selector.slice(npmIndex + 5);
|
|
512
450
|
}
|
|
513
|
-
|
|
514
|
-
|
|
451
|
+
const lastAt = selector.lastIndexOf("@");
|
|
452
|
+
if (lastAt <= 0) {
|
|
453
|
+
return null;
|
|
515
454
|
}
|
|
516
|
-
return
|
|
455
|
+
return selector.slice(lastAt + 1);
|
|
517
456
|
};
|
|
518
|
-
var
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (!node.isTypeOnly && node.moduleSpecifier !== void 0) {
|
|
532
|
-
const specifier = getSpecifierFromExpression(node.moduleSpecifier);
|
|
533
|
-
if (specifier !== void 0) {
|
|
534
|
-
specifiers.add(specifier);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
457
|
+
var parseYarnLock = (raw, directSpecs) => {
|
|
458
|
+
const lines = raw.split("\n");
|
|
459
|
+
const nodes = [];
|
|
460
|
+
let selectors = [];
|
|
461
|
+
let version2 = null;
|
|
462
|
+
let readingDependencies = false;
|
|
463
|
+
let dependencies = [];
|
|
464
|
+
const flushEntry = () => {
|
|
465
|
+
if (selectors.length === 0 || version2 === null) {
|
|
466
|
+
selectors = [];
|
|
467
|
+
version2 = null;
|
|
468
|
+
dependencies = [];
|
|
469
|
+
readingDependencies = false;
|
|
537
470
|
return;
|
|
538
471
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
specifiers.add(specifier);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
472
|
+
for (const selector of selectors) {
|
|
473
|
+
const parsedVersion = parseVersionSelector(selector);
|
|
474
|
+
const at = selector.lastIndexOf("@");
|
|
475
|
+
const name = at <= 0 ? selector : selector.slice(0, at);
|
|
476
|
+
if (name.length === 0) {
|
|
477
|
+
continue;
|
|
548
478
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
479
|
+
nodes.push({
|
|
480
|
+
name,
|
|
481
|
+
version: version2,
|
|
482
|
+
dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
|
|
483
|
+
});
|
|
484
|
+
if (parsedVersion !== null) {
|
|
485
|
+
nodes.push({
|
|
486
|
+
name,
|
|
487
|
+
version: parsedVersion,
|
|
488
|
+
dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
|
|
489
|
+
});
|
|
557
490
|
}
|
|
558
491
|
}
|
|
559
|
-
|
|
492
|
+
selectors = [];
|
|
493
|
+
version2 = null;
|
|
494
|
+
dependencies = [];
|
|
495
|
+
readingDependencies = false;
|
|
560
496
|
};
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
};
|
|
564
|
-
var parseTypescriptProject = (projectPath, onProgress) => {
|
|
565
|
-
const projectRoot = isAbsolute(projectPath) ? projectPath : resolve(projectPath);
|
|
566
|
-
const { fileNames, options, tsconfigCount, usedFallbackScan } = parseTsConfig(projectRoot);
|
|
567
|
-
onProgress?.({ stage: "config_resolved", tsconfigCount, usedFallbackScan });
|
|
568
|
-
const sourceFilePaths = fileNames.filter((filePath) => isProjectSourceFile(filePath, projectRoot)).map((filePath) => normalizePath(resolve(filePath)));
|
|
569
|
-
const uniqueSourceFilePaths = [...new Set(sourceFilePaths)].sort((a, b) => a.localeCompare(b));
|
|
570
|
-
const sourceFilePathSet = new Set(uniqueSourceFilePaths);
|
|
571
|
-
onProgress?.({ stage: "files_discovered", totalSourceFiles: uniqueSourceFilePaths.length });
|
|
572
|
-
const program2 = ts.createProgram({
|
|
573
|
-
rootNames: uniqueSourceFilePaths,
|
|
574
|
-
options
|
|
575
|
-
});
|
|
576
|
-
onProgress?.({ stage: "program_created", totalSourceFiles: uniqueSourceFilePaths.length });
|
|
577
|
-
const nodeByAbsolutePath = /* @__PURE__ */ new Map();
|
|
578
|
-
for (const sourcePath of uniqueSourceFilePaths) {
|
|
579
|
-
const relativePath = normalizePath(relative(projectRoot, sourcePath));
|
|
580
|
-
const nodeId = relativePath;
|
|
581
|
-
nodeByAbsolutePath.set(sourcePath, {
|
|
582
|
-
id: nodeId,
|
|
583
|
-
absolutePath: sourcePath,
|
|
584
|
-
relativePath
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
const resolverCache = /* @__PURE__ */ new Map();
|
|
588
|
-
const edges = [];
|
|
589
|
-
for (const [index, sourcePath] of uniqueSourceFilePaths.entries()) {
|
|
590
|
-
const sourceFile = program2.getSourceFile(sourcePath);
|
|
591
|
-
if (sourceFile === void 0) {
|
|
497
|
+
for (const line of lines) {
|
|
498
|
+
if (line.trim().length === 0) {
|
|
592
499
|
continue;
|
|
593
500
|
}
|
|
594
|
-
|
|
595
|
-
|
|
501
|
+
if (!line.startsWith(" ") && line.endsWith(":")) {
|
|
502
|
+
flushEntry();
|
|
503
|
+
const keyText = line.slice(0, -1);
|
|
504
|
+
selectors = keyText.split(",").map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
|
|
596
505
|
continue;
|
|
597
506
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const toNode = nodeByAbsolutePath.get(resolvedPath);
|
|
613
|
-
if (toNode === void 0) {
|
|
507
|
+
if (line.match(/^\s{2}version\s+/) !== null) {
|
|
508
|
+
const value = line.replace(/^\s{2}version\s+/, "").trim();
|
|
509
|
+
version2 = stripQuotes(value);
|
|
510
|
+
readingDependencies = false;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (line.match(/^\s{2}dependencies:\s*$/) !== null) {
|
|
514
|
+
readingDependencies = true;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (readingDependencies && line.match(/^\s{4}[^\s].+$/) !== null) {
|
|
518
|
+
const depLine = line.trim();
|
|
519
|
+
const firstSpace = depLine.indexOf(" ");
|
|
520
|
+
if (firstSpace <= 0) {
|
|
614
521
|
continue;
|
|
615
522
|
}
|
|
616
|
-
|
|
523
|
+
const depName = stripQuotes(depLine.slice(0, firstSpace));
|
|
524
|
+
const depRef = stripQuotes(depLine.slice(firstSpace + 1).trim());
|
|
525
|
+
const depVersion = parseVersionSelector(depRef) ?? depRef;
|
|
526
|
+
dependencies.push(`${depName}@${depVersion}`);
|
|
527
|
+
continue;
|
|
617
528
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
529
|
+
readingDependencies = false;
|
|
530
|
+
}
|
|
531
|
+
flushEntry();
|
|
532
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
533
|
+
for (const node of nodes) {
|
|
534
|
+
const key = `${node.name}@${node.version}`;
|
|
535
|
+
if (!deduped.has(key)) {
|
|
536
|
+
deduped.set(key, node);
|
|
626
537
|
}
|
|
627
538
|
}
|
|
628
|
-
onProgress?.({ stage: "edges_resolved", totalEdges: edges.length });
|
|
629
539
|
return {
|
|
630
|
-
|
|
631
|
-
|
|
540
|
+
kind: "yarn",
|
|
541
|
+
directDependencies: directSpecs,
|
|
542
|
+
nodes: [...deduped.values()].sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version))
|
|
632
543
|
};
|
|
633
544
|
};
|
|
634
|
-
var
|
|
635
|
-
|
|
636
|
-
const graphData = createGraphData(parsedProject.nodes, parsedProject.edges);
|
|
637
|
-
return createGraphAnalysisSummary(input.projectPath, graphData);
|
|
545
|
+
var parseBunLock = (_raw, _directSpecs) => {
|
|
546
|
+
throw new Error("unsupported_lockfile_format");
|
|
638
547
|
};
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
import { join } from "path";
|
|
643
|
-
var round4 = (value) => Number(value.toFixed(4));
|
|
644
|
-
var normalizeNodes = (nodes) => {
|
|
645
|
-
const byName = /* @__PURE__ */ new Map();
|
|
646
|
-
for (const node of nodes) {
|
|
647
|
-
const bucket = byName.get(node.name) ?? [];
|
|
648
|
-
bucket.push(node);
|
|
649
|
-
byName.set(node.name, bucket);
|
|
548
|
+
var parseRetryAfterMs = (value) => {
|
|
549
|
+
if (value === null) {
|
|
550
|
+
return null;
|
|
650
551
|
}
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
552
|
+
const seconds = Number.parseInt(value, 10);
|
|
553
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
return seconds * 1e3;
|
|
557
|
+
};
|
|
558
|
+
var shouldRetryStatus = (status) => status === 429 || status >= 500;
|
|
559
|
+
var fetchJsonWithRetry = async (url, options) => {
|
|
560
|
+
for (let attempt = 0; attempt <= options.retries; attempt += 1) {
|
|
561
|
+
const response = await fetch(url);
|
|
562
|
+
if (response.ok) {
|
|
563
|
+
return await response.json();
|
|
655
564
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
if (selected === void 0) {
|
|
659
|
-
continue;
|
|
565
|
+
if (!shouldRetryStatus(response.status) || attempt === options.retries) {
|
|
566
|
+
return null;
|
|
660
567
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}).filter((depName) => depName.length > 0).sort((a, b) => a.localeCompare(b));
|
|
665
|
-
normalized.push({
|
|
666
|
-
key: `${name}@${selected.version}`,
|
|
667
|
-
name,
|
|
668
|
-
version: selected.version,
|
|
669
|
-
dependencies: deps
|
|
670
|
-
});
|
|
568
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
569
|
+
const backoffMs = retryAfterMs ?? options.baseDelayMs * 2 ** attempt;
|
|
570
|
+
await sleep(backoffMs);
|
|
671
571
|
}
|
|
672
|
-
return
|
|
572
|
+
return null;
|
|
673
573
|
};
|
|
674
|
-
var
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
574
|
+
var MAX_RETRIES = 3;
|
|
575
|
+
var RETRY_BASE_DELAY_MS = 500;
|
|
576
|
+
var parsePrerelease = (value) => {
|
|
577
|
+
if (value === void 0 || value.length === 0) {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
return value.split(".").map((part) => {
|
|
581
|
+
const asNumber = Number.parseInt(part, 10);
|
|
582
|
+
if (!Number.isNaN(asNumber) && `${asNumber}` === part) {
|
|
583
|
+
return asNumber;
|
|
681
584
|
}
|
|
682
|
-
|
|
585
|
+
return part;
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
var parseSemver = (value) => {
|
|
589
|
+
const trimmed = value.trim();
|
|
590
|
+
const semverMatch = trimmed.match(
|
|
591
|
+
/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
592
|
+
);
|
|
593
|
+
if (semverMatch === null) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
const major = Number.parseInt(semverMatch[1] ?? "", 10);
|
|
597
|
+
const minor = Number.parseInt(semverMatch[2] ?? "", 10);
|
|
598
|
+
const patch = Number.parseInt(semverMatch[3] ?? "", 10);
|
|
599
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
major,
|
|
604
|
+
minor,
|
|
605
|
+
patch,
|
|
606
|
+
prerelease: parsePrerelease(semverMatch[4])
|
|
607
|
+
};
|
|
608
|
+
};
|
|
609
|
+
var compareIdentifier = (left, right) => {
|
|
610
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
611
|
+
return left - right;
|
|
612
|
+
}
|
|
613
|
+
if (typeof left === "number") {
|
|
614
|
+
return -1;
|
|
615
|
+
}
|
|
616
|
+
if (typeof right === "number") {
|
|
617
|
+
return 1;
|
|
618
|
+
}
|
|
619
|
+
return left.localeCompare(right);
|
|
620
|
+
};
|
|
621
|
+
var compareSemver = (left, right) => {
|
|
622
|
+
if (left.major !== right.major) {
|
|
623
|
+
return left.major - right.major;
|
|
624
|
+
}
|
|
625
|
+
if (left.minor !== right.minor) {
|
|
626
|
+
return left.minor - right.minor;
|
|
627
|
+
}
|
|
628
|
+
if (left.patch !== right.patch) {
|
|
629
|
+
return left.patch - right.patch;
|
|
630
|
+
}
|
|
631
|
+
if (left.prerelease.length === 0 && right.prerelease.length === 0) {
|
|
632
|
+
return 0;
|
|
633
|
+
}
|
|
634
|
+
if (left.prerelease.length === 0) {
|
|
635
|
+
return 1;
|
|
636
|
+
}
|
|
637
|
+
if (right.prerelease.length === 0) {
|
|
638
|
+
return -1;
|
|
639
|
+
}
|
|
640
|
+
const maxLength = Math.max(left.prerelease.length, right.prerelease.length);
|
|
641
|
+
for (let i = 0; i < maxLength; i += 1) {
|
|
642
|
+
const leftPart = left.prerelease[i];
|
|
643
|
+
const rightPart = right.prerelease[i];
|
|
644
|
+
if (leftPart === void 0 && rightPart === void 0) {
|
|
683
645
|
return 0;
|
|
684
646
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (node === void 0) {
|
|
688
|
-
visiting.delete(name);
|
|
689
|
-
depthByName.set(name, 0);
|
|
690
|
-
return 0;
|
|
647
|
+
if (leftPart === void 0) {
|
|
648
|
+
return -1;
|
|
691
649
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const childDepth = compute(dependencyName);
|
|
695
|
-
if (childDepth > maxChildDepth) {
|
|
696
|
-
maxChildDepth = childDepth;
|
|
697
|
-
}
|
|
650
|
+
if (rightPart === void 0) {
|
|
651
|
+
return 1;
|
|
698
652
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return ownDepth;
|
|
703
|
-
};
|
|
704
|
-
for (const name of nodeByName.keys()) {
|
|
705
|
-
compute(name);
|
|
706
|
-
}
|
|
707
|
-
let maxDepth = 0;
|
|
708
|
-
for (const depth of depthByName.values()) {
|
|
709
|
-
if (depth > maxDepth) {
|
|
710
|
-
maxDepth = depth;
|
|
653
|
+
const diff = compareIdentifier(leftPart, rightPart);
|
|
654
|
+
if (diff !== 0) {
|
|
655
|
+
return diff;
|
|
711
656
|
}
|
|
712
657
|
}
|
|
713
|
-
return
|
|
658
|
+
return 0;
|
|
714
659
|
};
|
|
715
|
-
var
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const stack = [...nodeByName.get(rootName)?.dependencies ?? []];
|
|
727
|
-
while (stack.length > 0) {
|
|
728
|
-
const current = stack.pop();
|
|
729
|
-
if (current === void 0 || seen.has(current) || current === rootName) {
|
|
730
|
-
continue;
|
|
660
|
+
var isWildcardPart = (value) => value === void 0 || value === "*" || value.toLowerCase() === "x";
|
|
661
|
+
var matchesPartialVersion = (version2, token) => {
|
|
662
|
+
const normalized = token.trim().replace(/^v/, "");
|
|
663
|
+
if (normalized === "*" || normalized.length === 0) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
const [majorPart, minorPart, patchPart] = normalized.split(".");
|
|
667
|
+
if (majorPart !== void 0 && !isWildcardPart(majorPart)) {
|
|
668
|
+
const major = Number.parseInt(majorPart, 10);
|
|
669
|
+
if (!Number.isFinite(major) || major !== version2.major) {
|
|
670
|
+
return false;
|
|
731
671
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
672
|
+
}
|
|
673
|
+
if (minorPart !== void 0 && !isWildcardPart(minorPart)) {
|
|
674
|
+
const minor = Number.parseInt(minorPart, 10);
|
|
675
|
+
if (!Number.isFinite(minor) || minor !== version2.minor) {
|
|
676
|
+
return false;
|
|
736
677
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
678
|
+
}
|
|
679
|
+
if (patchPart !== void 0 && !isWildcardPart(patchPart)) {
|
|
680
|
+
const patch = Number.parseInt(patchPart, 10);
|
|
681
|
+
if (!Number.isFinite(patch) || patch !== version2.patch) {
|
|
682
|
+
return false;
|
|
741
683
|
}
|
|
742
684
|
}
|
|
743
|
-
return
|
|
685
|
+
return true;
|
|
744
686
|
};
|
|
745
|
-
var
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
687
|
+
var parseComparatorToken = (token) => {
|
|
688
|
+
const operators = [">=", "<=", ">", "<", "="];
|
|
689
|
+
for (const operator of operators) {
|
|
690
|
+
if (token.startsWith(operator)) {
|
|
691
|
+
return {
|
|
692
|
+
operator,
|
|
693
|
+
versionToken: token.slice(operator.length).trim()
|
|
694
|
+
};
|
|
695
|
+
}
|
|
753
696
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
697
|
+
return {
|
|
698
|
+
operator: "=",
|
|
699
|
+
versionToken: token.trim()
|
|
700
|
+
};
|
|
701
|
+
};
|
|
702
|
+
var satisfiesComparator = (version2, token) => {
|
|
703
|
+
if (token.length === 0 || token === "*") {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
if (token.startsWith("^")) {
|
|
707
|
+
const base = parseSemver(token.slice(1));
|
|
708
|
+
if (base === null) {
|
|
709
|
+
return null;
|
|
760
710
|
}
|
|
711
|
+
let upper;
|
|
712
|
+
if (base.major > 0) {
|
|
713
|
+
upper = { major: base.major + 1, minor: 0, patch: 0, prerelease: [] };
|
|
714
|
+
} else if (base.minor > 0) {
|
|
715
|
+
upper = { major: 0, minor: base.minor + 1, patch: 0, prerelease: [] };
|
|
716
|
+
} else {
|
|
717
|
+
upper = { major: 0, minor: 0, patch: base.patch + 1, prerelease: [] };
|
|
718
|
+
}
|
|
719
|
+
return compareSemver(version2, base) >= 0 && compareSemver(version2, upper) < 0;
|
|
761
720
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
);
|
|
767
|
-
const allDependencies = [];
|
|
768
|
-
let metadataAvailableCount = 0;
|
|
769
|
-
for (const node of nodes) {
|
|
770
|
-
const metadata = metadataByKey.get(node.key) ?? null;
|
|
771
|
-
if (metadata !== null) {
|
|
772
|
-
metadataAvailableCount += 1;
|
|
721
|
+
if (token.startsWith("~")) {
|
|
722
|
+
const base = parseSemver(token.slice(1));
|
|
723
|
+
if (base === null) {
|
|
724
|
+
return null;
|
|
773
725
|
}
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
726
|
+
const upper = {
|
|
727
|
+
major: base.major,
|
|
728
|
+
minor: base.minor + 1,
|
|
729
|
+
patch: 0,
|
|
730
|
+
prerelease: []
|
|
731
|
+
};
|
|
732
|
+
return compareSemver(version2, base) >= 0 && compareSemver(version2, upper) < 0;
|
|
733
|
+
}
|
|
734
|
+
const parsedComparator = parseComparatorToken(token);
|
|
735
|
+
const hasWildcard = /(^|[.])(?:x|X|\*)($|[.])/.test(parsedComparator.versionToken);
|
|
736
|
+
if (hasWildcard) {
|
|
737
|
+
if (parsedComparator.operator !== "=") {
|
|
738
|
+
return null;
|
|
779
739
|
}
|
|
780
|
-
|
|
781
|
-
|
|
740
|
+
return matchesPartialVersion(version2, parsedComparator.versionToken);
|
|
741
|
+
}
|
|
742
|
+
const parsedVersion = parseSemver(parsedComparator.versionToken);
|
|
743
|
+
if (parsedVersion === null) {
|
|
744
|
+
if (parsedComparator.operator !== "=") {
|
|
745
|
+
return null;
|
|
782
746
|
}
|
|
783
|
-
|
|
784
|
-
|
|
747
|
+
return matchesPartialVersion(version2, parsedComparator.versionToken);
|
|
748
|
+
}
|
|
749
|
+
const comparison = compareSemver(version2, parsedVersion);
|
|
750
|
+
switch (parsedComparator.operator) {
|
|
751
|
+
case ">":
|
|
752
|
+
return comparison > 0;
|
|
753
|
+
case ">=":
|
|
754
|
+
return comparison >= 0;
|
|
755
|
+
case "<":
|
|
756
|
+
return comparison < 0;
|
|
757
|
+
case "<=":
|
|
758
|
+
return comparison <= 0;
|
|
759
|
+
case "=":
|
|
760
|
+
return comparison === 0;
|
|
761
|
+
default:
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
var satisfiesRangeClause = (version2, clause) => {
|
|
766
|
+
const hyphenMatch = clause.match(/^\s*(.+?)\s+-\s+(.+?)\s*$/);
|
|
767
|
+
if (hyphenMatch !== null) {
|
|
768
|
+
const lower = hyphenMatch[1];
|
|
769
|
+
const upper = hyphenMatch[2];
|
|
770
|
+
if (lower === void 0 || upper === void 0) {
|
|
771
|
+
return null;
|
|
785
772
|
}
|
|
786
|
-
|
|
787
|
-
|
|
773
|
+
const lowerResult = satisfiesComparator(version2, `>=${lower}`);
|
|
774
|
+
const upperResult = satisfiesComparator(version2, `<=${upper}`);
|
|
775
|
+
if (lowerResult === null || upperResult === null) {
|
|
776
|
+
return null;
|
|
788
777
|
}
|
|
789
|
-
|
|
790
|
-
|
|
778
|
+
return lowerResult && upperResult;
|
|
779
|
+
}
|
|
780
|
+
const tokens = clause.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
781
|
+
if (tokens.length === 0) {
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
for (const token of tokens) {
|
|
785
|
+
const matched = satisfiesComparator(version2, token);
|
|
786
|
+
if (matched === null) {
|
|
787
|
+
return null;
|
|
791
788
|
}
|
|
792
|
-
if (
|
|
793
|
-
|
|
789
|
+
if (!matched) {
|
|
790
|
+
return false;
|
|
794
791
|
}
|
|
795
|
-
allDependencies.push({
|
|
796
|
-
name: node.name,
|
|
797
|
-
direct: directNames.has(node.name),
|
|
798
|
-
requestedRange: directSpecByName.get(node.name) ?? null,
|
|
799
|
-
resolvedVersion: node.version,
|
|
800
|
-
transitiveDependencies: [],
|
|
801
|
-
dependencyDepth,
|
|
802
|
-
fanOut: node.dependencies.length,
|
|
803
|
-
dependents,
|
|
804
|
-
maintainerCount: metadata?.maintainerCount ?? null,
|
|
805
|
-
releaseFrequencyDays: metadata?.releaseFrequencyDays ?? null,
|
|
806
|
-
daysSinceLastRelease: metadata?.daysSinceLastRelease ?? null,
|
|
807
|
-
repositoryActivity30d: metadata?.repositoryActivity30d ?? null,
|
|
808
|
-
busFactor: metadata?.busFactor ?? null,
|
|
809
|
-
ownRiskSignals: [...riskSignals].sort((a, b) => a.localeCompare(b)),
|
|
810
|
-
inheritedRiskSignals: [],
|
|
811
|
-
riskSignals
|
|
812
|
-
});
|
|
813
792
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
793
|
+
return true;
|
|
794
|
+
};
|
|
795
|
+
var resolveRangeVersion = (versions, requested) => {
|
|
796
|
+
const clauses = requested.split("||").map((clause) => clause.trim()).filter((clause) => clause.length > 0);
|
|
797
|
+
if (clauses.length === 0) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
const parsedVersions = versions.map((version2) => ({ version: version2, parsed: parseSemver(version2) })).filter((candidate) => candidate.parsed !== null).sort((a, b) => compareSemver(b.parsed, a.parsed));
|
|
801
|
+
for (const candidate of parsedVersions) {
|
|
802
|
+
let clauseMatched = false;
|
|
803
|
+
let clauseUnsupported = false;
|
|
804
|
+
for (const clause of clauses) {
|
|
805
|
+
const matched = satisfiesRangeClause(candidate.parsed, clause);
|
|
806
|
+
if (matched === null) {
|
|
807
|
+
clauseUnsupported = true;
|
|
823
808
|
continue;
|
|
824
809
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
allSignals.add(signal);
|
|
829
|
-
}
|
|
810
|
+
if (matched) {
|
|
811
|
+
clauseMatched = true;
|
|
812
|
+
break;
|
|
830
813
|
}
|
|
831
814
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const singleMaintainerDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("single_maintainer")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
841
|
-
const abandonedDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("abandoned")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
842
|
-
return {
|
|
843
|
-
targetPath,
|
|
844
|
-
available: true,
|
|
845
|
-
metrics: {
|
|
846
|
-
totalDependencies: allDependencies.length,
|
|
847
|
-
directDependencies: dependencies.length,
|
|
848
|
-
transitiveDependencies: allDependencies.length - dependencies.length,
|
|
849
|
-
dependencyDepth: maxDepth,
|
|
850
|
-
lockfileKind: extraction.kind,
|
|
851
|
-
metadataCoverage: allDependencies.length === 0 ? 0 : round4(metadataAvailableCount / allDependencies.length)
|
|
852
|
-
},
|
|
853
|
-
dependencies,
|
|
854
|
-
highRiskDependencies,
|
|
855
|
-
singleMaintainerDependencies,
|
|
856
|
-
abandonedDependencies,
|
|
857
|
-
centralityRanking
|
|
858
|
-
};
|
|
859
|
-
};
|
|
860
|
-
var DEFAULT_EXTERNAL_ANALYSIS_CONFIG = {
|
|
861
|
-
abandonedDaysThreshold: 540,
|
|
862
|
-
deepChainThreshold: 6,
|
|
863
|
-
fanOutHighThreshold: 25,
|
|
864
|
-
centralityTopN: 20,
|
|
865
|
-
maxHighRiskDependencies: 100,
|
|
866
|
-
metadataRequestConcurrency: 8
|
|
815
|
+
if (clauseMatched) {
|
|
816
|
+
return candidate.version;
|
|
817
|
+
}
|
|
818
|
+
if (clauseUnsupported && clauses.length === 1) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return null;
|
|
867
823
|
};
|
|
868
|
-
var
|
|
869
|
-
|
|
870
|
-
{
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
var loadPackageJson = (repositoryPath) => {
|
|
877
|
-
const packageJsonPath2 = join(repositoryPath, "package.json");
|
|
878
|
-
if (!existsSync(packageJsonPath2)) {
|
|
824
|
+
var fetchPackument = async (name) => {
|
|
825
|
+
const encodedName = encodeURIComponent(name);
|
|
826
|
+
try {
|
|
827
|
+
return await fetchJsonWithRetry(`https://registry.npmjs.org/${encodedName}`, {
|
|
828
|
+
retries: MAX_RETRIES,
|
|
829
|
+
baseDelayMs: RETRY_BASE_DELAY_MS
|
|
830
|
+
});
|
|
831
|
+
} catch {
|
|
879
832
|
return null;
|
|
880
833
|
}
|
|
881
|
-
return {
|
|
882
|
-
path: packageJsonPath2,
|
|
883
|
-
raw: readFileSync(packageJsonPath2, "utf8")
|
|
884
|
-
};
|
|
885
834
|
};
|
|
886
|
-
var
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
835
|
+
var resolveRequestedVersion = (packument, requested) => {
|
|
836
|
+
const versions = packument.versions ?? {};
|
|
837
|
+
const versionKeys = Object.keys(versions);
|
|
838
|
+
const tags = packument["dist-tags"] ?? {};
|
|
839
|
+
const latest = tags["latest"];
|
|
840
|
+
if (requested !== null && versions[requested] !== void 0) {
|
|
892
841
|
return {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
842
|
+
version: requested,
|
|
843
|
+
resolution: "exact",
|
|
844
|
+
fallbackUsed: false
|
|
896
845
|
};
|
|
897
846
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
parsed.optionalDependencies,
|
|
907
|
-
parsed.peerDependencies
|
|
908
|
-
]) {
|
|
909
|
-
if (block === void 0) {
|
|
910
|
-
continue;
|
|
847
|
+
if (requested !== null) {
|
|
848
|
+
const tagged = tags[requested];
|
|
849
|
+
if (tagged !== void 0 && versions[tagged] !== void 0) {
|
|
850
|
+
return {
|
|
851
|
+
version: tagged,
|
|
852
|
+
resolution: "tag",
|
|
853
|
+
fallbackUsed: false
|
|
854
|
+
};
|
|
911
855
|
}
|
|
912
|
-
for (const [name, versionRange] of Object.entries(block)) {
|
|
913
|
-
merged.set(name, versionRange);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
return [...merged.entries()].map(([name, requestedRange]) => ({ name, requestedRange })).sort((a, b) => a.name.localeCompare(b.name));
|
|
917
|
-
};
|
|
918
|
-
var parsePackageLock = (raw, directSpecs) => {
|
|
919
|
-
const parsed = JSON.parse(raw);
|
|
920
|
-
const nodes = [];
|
|
921
|
-
if (parsed.packages !== void 0) {
|
|
922
|
-
for (const [packagePath, packageData] of Object.entries(parsed.packages)) {
|
|
923
|
-
if (packagePath.length === 0 || packageData.version === void 0) {
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
const segments = packagePath.split("node_modules/");
|
|
927
|
-
const name = segments[segments.length - 1] ?? "";
|
|
928
|
-
if (name.length === 0) {
|
|
929
|
-
continue;
|
|
930
|
-
}
|
|
931
|
-
const dependencies = Object.entries(packageData.dependencies ?? {}).map(([depName, depRange]) => `${depName}@${String(depRange)}`).sort((a, b) => a.localeCompare(b));
|
|
932
|
-
nodes.push({
|
|
933
|
-
name,
|
|
934
|
-
version: packageData.version,
|
|
935
|
-
dependencies
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
} else if (parsed.dependencies !== void 0) {
|
|
939
|
-
for (const [name, dep] of Object.entries(parsed.dependencies)) {
|
|
940
|
-
if (dep.version === void 0) {
|
|
941
|
-
continue;
|
|
942
|
-
}
|
|
943
|
-
const dependencies = Object.entries(dep.dependencies ?? {}).map(([depName, depVersion]) => `${depName}@${String(depVersion)}`).sort((a, b) => a.localeCompare(b));
|
|
944
|
-
nodes.push({
|
|
945
|
-
name,
|
|
946
|
-
version: dep.version,
|
|
947
|
-
dependencies
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
nodes.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
952
|
-
return {
|
|
953
|
-
kind: "npm",
|
|
954
|
-
directDependencies: directSpecs,
|
|
955
|
-
nodes
|
|
956
|
-
};
|
|
957
|
-
};
|
|
958
|
-
var sanitizeValue = (value) => value.replace(/^['"]|['"]$/g, "").trim();
|
|
959
|
-
var parsePackageKey = (rawKey) => {
|
|
960
|
-
const key = sanitizeValue(rawKey.replace(/:$/, ""));
|
|
961
|
-
const withoutSlash = key.startsWith("/") ? key.slice(1) : key;
|
|
962
|
-
const lastAt = withoutSlash.lastIndexOf("@");
|
|
963
|
-
if (lastAt <= 0) {
|
|
964
|
-
return null;
|
|
965
|
-
}
|
|
966
|
-
const name = withoutSlash.slice(0, lastAt);
|
|
967
|
-
const versionWithPeers = withoutSlash.slice(lastAt + 1);
|
|
968
|
-
const version2 = versionWithPeers.split("(")[0] ?? versionWithPeers;
|
|
969
|
-
if (name.length === 0 || version2.length === 0) {
|
|
970
|
-
return null;
|
|
971
856
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
for (const line of lines) {
|
|
981
|
-
if (line.trim().length === 0 || line.trimStart().startsWith("#")) {
|
|
982
|
-
continue;
|
|
983
|
-
}
|
|
984
|
-
if (line.startsWith("importers:")) {
|
|
985
|
-
state = "importers";
|
|
986
|
-
continue;
|
|
987
|
-
}
|
|
988
|
-
if (line.startsWith("packages:")) {
|
|
989
|
-
state = "packages";
|
|
990
|
-
continue;
|
|
991
|
-
}
|
|
992
|
-
if (state === "packages" || state === "packageDeps") {
|
|
993
|
-
const packageMatch = line.match(/^\s{2}([^\s].+):\s*$/);
|
|
994
|
-
if (packageMatch !== null) {
|
|
995
|
-
const parsedKey = parsePackageKey(packageMatch[1] ?? "");
|
|
996
|
-
if (parsedKey !== null) {
|
|
997
|
-
currentPackage = `${parsedKey.name}@${parsedKey.version}`;
|
|
998
|
-
dependenciesByNode.set(currentPackage, /* @__PURE__ */ new Set());
|
|
999
|
-
state = "packageDeps";
|
|
1000
|
-
currentDependencyName = null;
|
|
1001
|
-
}
|
|
1002
|
-
continue;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
if (state === "packageDeps" && currentPackage !== null) {
|
|
1006
|
-
const depLine = line.match(/^\s{6}([^:\s]+):\s*(.+)$/);
|
|
1007
|
-
if (depLine !== null) {
|
|
1008
|
-
const depName = sanitizeValue(depLine[1] ?? "");
|
|
1009
|
-
const depRef = sanitizeValue(depLine[2] ?? "");
|
|
1010
|
-
const depVersion = depRef.split("(")[0] ?? depRef;
|
|
1011
|
-
if (depName.length > 0 && depVersion.length > 0) {
|
|
1012
|
-
dependenciesByNode.get(currentPackage)?.add(`${depName}@${depVersion}`);
|
|
1013
|
-
}
|
|
1014
|
-
currentDependencyName = null;
|
|
1015
|
-
continue;
|
|
1016
|
-
}
|
|
1017
|
-
const depBlockLine = line.match(/^\s{6}([^:\s]+):\s*$/);
|
|
1018
|
-
if (depBlockLine !== null) {
|
|
1019
|
-
currentDependencyName = sanitizeValue(depBlockLine[1] ?? "");
|
|
1020
|
-
continue;
|
|
1021
|
-
}
|
|
1022
|
-
const depVersionLine = line.match(/^\s{8}version:\s*(.+)$/);
|
|
1023
|
-
if (depVersionLine !== null && currentDependencyName !== null) {
|
|
1024
|
-
const depRef = sanitizeValue(depVersionLine[1] ?? "");
|
|
1025
|
-
const depVersion = depRef.split("(")[0] ?? depRef;
|
|
1026
|
-
if (depVersion.length > 0) {
|
|
1027
|
-
dependenciesByNode.get(currentPackage)?.add(`${currentDependencyName}@${depVersion}`);
|
|
1028
|
-
}
|
|
1029
|
-
currentDependencyName = null;
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
if (line.match(/^\s{4}(dependencies|optionalDependencies):\s*$/) !== null) {
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
857
|
+
if (requested !== null) {
|
|
858
|
+
const matched = resolveRangeVersion(versionKeys, requested);
|
|
859
|
+
if (matched !== null && versions[matched] !== void 0) {
|
|
860
|
+
return {
|
|
861
|
+
version: matched,
|
|
862
|
+
resolution: "range",
|
|
863
|
+
fallbackUsed: false
|
|
864
|
+
};
|
|
1035
865
|
}
|
|
1036
866
|
}
|
|
1037
|
-
|
|
1038
|
-
const at = nodeId.lastIndexOf("@");
|
|
867
|
+
if (latest !== void 0 && versions[latest] !== void 0) {
|
|
1039
868
|
return {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
869
|
+
version: latest,
|
|
870
|
+
resolution: "latest",
|
|
871
|
+
fallbackUsed: requested !== null
|
|
1043
872
|
};
|
|
1044
|
-
}).sort(
|
|
1045
|
-
(a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version)
|
|
1046
|
-
);
|
|
1047
|
-
return {
|
|
1048
|
-
kind: "pnpm",
|
|
1049
|
-
directDependencies: directSpecs,
|
|
1050
|
-
nodes
|
|
1051
|
-
};
|
|
1052
|
-
};
|
|
1053
|
-
var stripQuotes = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
1054
|
-
var parseVersionSelector = (selector) => {
|
|
1055
|
-
const npmIndex = selector.lastIndexOf("@npm:");
|
|
1056
|
-
if (npmIndex >= 0) {
|
|
1057
|
-
return selector.slice(npmIndex + 5);
|
|
1058
873
|
}
|
|
1059
|
-
const
|
|
1060
|
-
|
|
874
|
+
const semverSorted = versionKeys.map((version2) => ({ version: version2, parsed: parseSemver(version2) })).filter((candidate) => candidate.parsed !== null).sort((a, b) => compareSemver(b.parsed, a.parsed)).map((candidate) => candidate.version);
|
|
875
|
+
const fallbackVersion = semverSorted[0] ?? versionKeys.sort((a, b) => b.localeCompare(a))[0];
|
|
876
|
+
if (fallbackVersion === void 0) {
|
|
1061
877
|
return null;
|
|
1062
878
|
}
|
|
1063
|
-
return
|
|
879
|
+
return {
|
|
880
|
+
version: fallbackVersion,
|
|
881
|
+
resolution: "latest",
|
|
882
|
+
fallbackUsed: requested !== null
|
|
883
|
+
};
|
|
1064
884
|
};
|
|
1065
|
-
var
|
|
1066
|
-
const
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
});
|
|
885
|
+
var resolveRegistryGraphFromDirectSpecs = async (directSpecs, options) => {
|
|
886
|
+
const maxNodes = Math.max(1, options.maxNodes);
|
|
887
|
+
const maxDepth = Math.max(0, options.maxDepth);
|
|
888
|
+
const queue = directSpecs.map((spec) => ({
|
|
889
|
+
name: spec.name,
|
|
890
|
+
requested: spec.requestedRange,
|
|
891
|
+
depth: 0
|
|
892
|
+
}));
|
|
893
|
+
const scopeByName = new Map(directSpecs.map((spec) => [spec.name, spec.scope]));
|
|
894
|
+
const requestedByName = new Map(directSpecs.map((spec) => [spec.name, spec.requestedRange]));
|
|
895
|
+
const packumentByName = /* @__PURE__ */ new Map();
|
|
896
|
+
const nodesByKey = /* @__PURE__ */ new Map();
|
|
897
|
+
const directByName = /* @__PURE__ */ new Map();
|
|
898
|
+
const assumptions = /* @__PURE__ */ new Set();
|
|
899
|
+
let truncated = false;
|
|
900
|
+
while (queue.length > 0) {
|
|
901
|
+
if (nodesByKey.size >= maxNodes) {
|
|
902
|
+
truncated = true;
|
|
903
|
+
assumptions.add(`Dependency graph truncated at ${maxNodes} nodes.`);
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
const item = queue.shift();
|
|
907
|
+
if (item === void 0) {
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
let packument = packumentByName.get(item.name) ?? null;
|
|
911
|
+
if (!packumentByName.has(item.name)) {
|
|
912
|
+
packument = await fetchPackument(item.name);
|
|
913
|
+
packumentByName.set(item.name, packument);
|
|
914
|
+
}
|
|
915
|
+
if (packument === null) {
|
|
916
|
+
if (scopeByName.has(item.name)) {
|
|
917
|
+
assumptions.add(`Could not resolve direct dependency from registry: ${item.name}.`);
|
|
1098
918
|
}
|
|
1099
|
-
}
|
|
1100
|
-
selectors = [];
|
|
1101
|
-
version2 = null;
|
|
1102
|
-
dependencies = [];
|
|
1103
|
-
readingDependencies = false;
|
|
1104
|
-
};
|
|
1105
|
-
for (const line of lines) {
|
|
1106
|
-
if (line.trim().length === 0) {
|
|
1107
919
|
continue;
|
|
1108
920
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
921
|
+
const resolved = resolveRequestedVersion(packument, item.requested);
|
|
922
|
+
if (resolved === null) {
|
|
923
|
+
if (scopeByName.has(item.name)) {
|
|
924
|
+
assumptions.add(`Could not resolve direct dependency version: ${item.name}.`);
|
|
925
|
+
}
|
|
1113
926
|
continue;
|
|
1114
927
|
}
|
|
1115
|
-
if (
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
928
|
+
if (scopeByName.has(item.name) && !directByName.has(item.name)) {
|
|
929
|
+
directByName.set(item.name, {
|
|
930
|
+
name: item.name,
|
|
931
|
+
requestedRange: requestedByName.get(item.name) ?? "latest",
|
|
932
|
+
resolvedVersion: resolved.version,
|
|
933
|
+
resolution: resolved.resolution,
|
|
934
|
+
scope: scopeByName.get(item.name) ?? "prod"
|
|
935
|
+
});
|
|
1120
936
|
}
|
|
1121
|
-
if (
|
|
1122
|
-
|
|
937
|
+
if (resolved.fallbackUsed && item.requested !== null) {
|
|
938
|
+
assumptions.add(
|
|
939
|
+
`Resolved ${item.name}@${item.requested} to latest (${resolved.version}) because exact/tag/range match was unavailable.`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
const nodeKey = `${item.name}@${resolved.version}`;
|
|
943
|
+
if (nodesByKey.has(nodeKey)) {
|
|
1123
944
|
continue;
|
|
1124
945
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
946
|
+
const manifest = (packument.versions ?? {})[resolved.version] ?? {};
|
|
947
|
+
const dependencies = Object.entries(manifest.dependencies ?? {}).filter(([dependencyName, dependencyRange]) => dependencyName.length > 0 && dependencyRange.length > 0).sort((a, b) => a[0].localeCompare(b[0]));
|
|
948
|
+
nodesByKey.set(nodeKey, {
|
|
949
|
+
name: item.name,
|
|
950
|
+
version: resolved.version,
|
|
951
|
+
dependencies: dependencies.map(([dependencyName, dependencyRange]) => `${dependencyName}@${dependencyRange}`)
|
|
952
|
+
});
|
|
953
|
+
if (item.depth >= maxDepth && dependencies.length > 0) {
|
|
954
|
+
truncated = true;
|
|
955
|
+
assumptions.add(`Dependency graph truncated at depth ${maxDepth}.`);
|
|
1135
956
|
continue;
|
|
1136
957
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
958
|
+
for (const [dependencyName, dependencyRange] of dependencies) {
|
|
959
|
+
if (nodesByKey.size + queue.length >= maxNodes) {
|
|
960
|
+
truncated = true;
|
|
961
|
+
assumptions.add(`Dependency graph truncated at ${maxNodes} nodes.`);
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
queue.push({
|
|
965
|
+
name: dependencyName,
|
|
966
|
+
requested: dependencyRange,
|
|
967
|
+
depth: item.depth + 1
|
|
968
|
+
});
|
|
1145
969
|
}
|
|
1146
970
|
}
|
|
1147
971
|
return {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
972
|
+
nodes: [...nodesByKey.values()].sort(
|
|
973
|
+
(a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version)
|
|
974
|
+
),
|
|
975
|
+
directDependencies: [...directByName.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
976
|
+
assumptions: [...assumptions].sort((a, b) => a.localeCompare(b)),
|
|
977
|
+
truncated
|
|
1151
978
|
};
|
|
1152
979
|
};
|
|
1153
|
-
var parseBunLock = (_raw, _directSpecs) => {
|
|
1154
|
-
throw new Error("unsupported_lockfile_format");
|
|
1155
|
-
};
|
|
1156
980
|
var withDefaults = (overrides) => ({
|
|
1157
981
|
...DEFAULT_EXTERNAL_ANALYSIS_CONFIG,
|
|
1158
982
|
...overrides
|
|
@@ -1177,26 +1001,27 @@ var parseExtraction = (lockfileKind, lockfileRaw, directSpecs) => {
|
|
|
1177
1001
|
};
|
|
1178
1002
|
var mapWithConcurrency = async (values, limit, handler) => {
|
|
1179
1003
|
const effectiveLimit = Math.max(1, limit);
|
|
1004
|
+
const workerCount = Math.min(effectiveLimit, values.length);
|
|
1180
1005
|
const results = new Array(values.length);
|
|
1181
1006
|
let index = 0;
|
|
1182
|
-
const workers = Array.from({ length:
|
|
1183
|
-
|
|
1007
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
1008
|
+
while (true) {
|
|
1184
1009
|
const current = index;
|
|
1185
1010
|
index += 1;
|
|
1186
1011
|
if (current >= values.length) {
|
|
1187
1012
|
return;
|
|
1188
1013
|
}
|
|
1189
1014
|
const value = values[current];
|
|
1190
|
-
if (value
|
|
1191
|
-
|
|
1015
|
+
if (value !== void 0) {
|
|
1016
|
+
results[current] = await handler(value);
|
|
1192
1017
|
}
|
|
1193
|
-
results[current] = await handler(value);
|
|
1194
1018
|
}
|
|
1195
1019
|
});
|
|
1196
1020
|
await Promise.all(workers);
|
|
1197
1021
|
return results;
|
|
1198
1022
|
};
|
|
1199
1023
|
var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
|
|
1024
|
+
const config = withDefaults(input.config);
|
|
1200
1025
|
const packageJson = loadPackageJson(input.repositoryPath);
|
|
1201
1026
|
if (packageJson === null) {
|
|
1202
1027
|
return {
|
|
@@ -1206,19 +1031,37 @@ var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
|
|
|
1206
1031
|
};
|
|
1207
1032
|
}
|
|
1208
1033
|
onProgress?.({ stage: "package_json_loaded" });
|
|
1209
|
-
const lockfile = selectLockfile(input.repositoryPath);
|
|
1210
|
-
if (lockfile === null) {
|
|
1211
|
-
return {
|
|
1212
|
-
targetPath: input.repositoryPath,
|
|
1213
|
-
available: false,
|
|
1214
|
-
reason: "lockfile_not_found"
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
onProgress?.({ stage: "lockfile_selected", kind: lockfile.kind });
|
|
1218
1034
|
try {
|
|
1219
1035
|
const directSpecs = parsePackageJson(packageJson.raw);
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1036
|
+
const lockfile = selectLockfile(input.repositoryPath);
|
|
1037
|
+
let extraction;
|
|
1038
|
+
if (lockfile === null) {
|
|
1039
|
+
const resolvedGraph = await resolveRegistryGraphFromDirectSpecs(directSpecs, {
|
|
1040
|
+
maxNodes: 500,
|
|
1041
|
+
maxDepth: 8
|
|
1042
|
+
});
|
|
1043
|
+
if (resolvedGraph.nodes.length === 0) {
|
|
1044
|
+
return {
|
|
1045
|
+
targetPath: input.repositoryPath,
|
|
1046
|
+
available: false,
|
|
1047
|
+
reason: "lockfile_not_found"
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
extraction = {
|
|
1051
|
+
kind: "npm",
|
|
1052
|
+
directDependencies: resolvedGraph.directDependencies.map((dependency) => ({
|
|
1053
|
+
name: dependency.name,
|
|
1054
|
+
requestedRange: dependency.requestedRange,
|
|
1055
|
+
scope: dependency.scope
|
|
1056
|
+
})),
|
|
1057
|
+
nodes: resolvedGraph.nodes
|
|
1058
|
+
};
|
|
1059
|
+
onProgress?.({ stage: "lockfile_selected", kind: "npm" });
|
|
1060
|
+
} else {
|
|
1061
|
+
extraction = parseExtraction(lockfile.kind, lockfile.raw, directSpecs);
|
|
1062
|
+
onProgress?.({ stage: "lockfile_selected", kind: lockfile.kind });
|
|
1063
|
+
}
|
|
1064
|
+
const directNames = new Set(extraction.directDependencies.map((dependency) => dependency.name));
|
|
1222
1065
|
onProgress?.({
|
|
1223
1066
|
stage: "lockfile_parsed",
|
|
1224
1067
|
dependencyNodes: extraction.nodes.length,
|
|
@@ -1232,7 +1075,9 @@ var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
|
|
|
1232
1075
|
async (node) => {
|
|
1233
1076
|
const result = {
|
|
1234
1077
|
key: `${node.name}@${node.version}`,
|
|
1235
|
-
metadata: await metadataProvider.getMetadata(node.name, node.version
|
|
1078
|
+
metadata: await metadataProvider.getMetadata(node.name, node.version, {
|
|
1079
|
+
directDependency: directNames.has(node.name)
|
|
1080
|
+
})
|
|
1236
1081
|
};
|
|
1237
1082
|
completed += 1;
|
|
1238
1083
|
onProgress?.({
|
|
@@ -1268,81 +1113,907 @@ var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
|
|
|
1268
1113
|
};
|
|
1269
1114
|
}
|
|
1270
1115
|
return {
|
|
1271
|
-
targetPath: input.repositoryPath,
|
|
1272
|
-
available: false,
|
|
1273
|
-
reason: "invalid_lockfile"
|
|
1116
|
+
targetPath: input.repositoryPath,
|
|
1117
|
+
available: false,
|
|
1118
|
+
reason: "invalid_lockfile"
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
var DEFAULT_MAX_NODES = 250;
|
|
1123
|
+
var DEFAULT_MAX_DEPTH = 6;
|
|
1124
|
+
var withDefaults2 = (overrides) => ({
|
|
1125
|
+
...DEFAULT_EXTERNAL_ANALYSIS_CONFIG,
|
|
1126
|
+
...overrides
|
|
1127
|
+
});
|
|
1128
|
+
var parseDependencySpec = (value) => {
|
|
1129
|
+
const trimmed = value.trim();
|
|
1130
|
+
if (trimmed.length === 0 || /\s/.test(trimmed)) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
if (trimmed.startsWith("@")) {
|
|
1134
|
+
const splitIndex2 = trimmed.lastIndexOf("@");
|
|
1135
|
+
if (splitIndex2 <= 0) {
|
|
1136
|
+
return { name: trimmed, requested: null };
|
|
1137
|
+
}
|
|
1138
|
+
const name2 = trimmed.slice(0, splitIndex2);
|
|
1139
|
+
const requested2 = trimmed.slice(splitIndex2 + 1);
|
|
1140
|
+
if (name2.length === 0 || requested2.length === 0) {
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
return { name: name2, requested: requested2 };
|
|
1144
|
+
}
|
|
1145
|
+
const splitIndex = trimmed.lastIndexOf("@");
|
|
1146
|
+
if (splitIndex <= 0) {
|
|
1147
|
+
return { name: trimmed, requested: null };
|
|
1148
|
+
}
|
|
1149
|
+
const name = trimmed.slice(0, splitIndex);
|
|
1150
|
+
const requested = trimmed.slice(splitIndex + 1);
|
|
1151
|
+
if (name.length === 0 || requested.length === 0) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
return { name, requested };
|
|
1155
|
+
};
|
|
1156
|
+
var mapWithConcurrency2 = async (values, limit, handler) => {
|
|
1157
|
+
const effectiveLimit = Math.max(1, limit);
|
|
1158
|
+
const workerCount = Math.min(effectiveLimit, values.length);
|
|
1159
|
+
const results = new Array(values.length);
|
|
1160
|
+
let index = 0;
|
|
1161
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
1162
|
+
while (true) {
|
|
1163
|
+
const current = index;
|
|
1164
|
+
index += 1;
|
|
1165
|
+
if (current >= values.length) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const value = values[current];
|
|
1169
|
+
if (value !== void 0) {
|
|
1170
|
+
results[current] = await handler(value);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
await Promise.all(workers);
|
|
1175
|
+
return results;
|
|
1176
|
+
};
|
|
1177
|
+
var analyzeDependencyCandidate = async (input, metadataProvider) => {
|
|
1178
|
+
const parsed = parseDependencySpec(input.dependency);
|
|
1179
|
+
if (parsed === null) {
|
|
1180
|
+
return {
|
|
1181
|
+
available: false,
|
|
1182
|
+
reason: "invalid_dependency_spec",
|
|
1183
|
+
dependency: input.dependency
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
const maxNodes = Math.max(1, input.maxNodes ?? DEFAULT_MAX_NODES);
|
|
1187
|
+
const maxDepth = Math.max(0, input.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
1188
|
+
const config = withDefaults2(input.config);
|
|
1189
|
+
const graph = await resolveRegistryGraphFromDirectSpecs(
|
|
1190
|
+
[
|
|
1191
|
+
{
|
|
1192
|
+
name: parsed.name,
|
|
1193
|
+
requestedRange: parsed.requested ?? "latest",
|
|
1194
|
+
scope: "prod"
|
|
1195
|
+
}
|
|
1196
|
+
],
|
|
1197
|
+
{ maxNodes, maxDepth }
|
|
1198
|
+
);
|
|
1199
|
+
const direct = graph.directDependencies.find((dependency) => dependency.name === parsed.name);
|
|
1200
|
+
if (direct === void 0 || graph.nodes.length === 0) {
|
|
1201
|
+
return {
|
|
1202
|
+
available: false,
|
|
1203
|
+
reason: "package_not_found",
|
|
1204
|
+
dependency: input.dependency
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
const metadataEntries = await mapWithConcurrency2(
|
|
1208
|
+
graph.nodes,
|
|
1209
|
+
config.metadataRequestConcurrency,
|
|
1210
|
+
async (node) => ({
|
|
1211
|
+
key: `${node.name}@${node.version}`,
|
|
1212
|
+
metadata: await metadataProvider.getMetadata(node.name, node.version, {
|
|
1213
|
+
directDependency: node.name === parsed.name
|
|
1214
|
+
})
|
|
1215
|
+
})
|
|
1216
|
+
);
|
|
1217
|
+
const metadataByKey = /* @__PURE__ */ new Map();
|
|
1218
|
+
for (const entry of metadataEntries) {
|
|
1219
|
+
metadataByKey.set(entry.key, entry.metadata);
|
|
1220
|
+
}
|
|
1221
|
+
const extraction = {
|
|
1222
|
+
kind: "npm",
|
|
1223
|
+
directDependencies: [
|
|
1224
|
+
{
|
|
1225
|
+
name: parsed.name,
|
|
1226
|
+
requestedRange: parsed.requested ?? "latest",
|
|
1227
|
+
scope: "prod"
|
|
1228
|
+
}
|
|
1229
|
+
],
|
|
1230
|
+
nodes: graph.nodes
|
|
1231
|
+
};
|
|
1232
|
+
const external = buildExternalAnalysisSummary(
|
|
1233
|
+
`npm:${parsed.name}`,
|
|
1234
|
+
extraction,
|
|
1235
|
+
metadataByKey,
|
|
1236
|
+
config
|
|
1237
|
+
);
|
|
1238
|
+
return {
|
|
1239
|
+
available: true,
|
|
1240
|
+
dependency: {
|
|
1241
|
+
name: parsed.name,
|
|
1242
|
+
requested: parsed.requested,
|
|
1243
|
+
resolvedVersion: direct.resolvedVersion,
|
|
1244
|
+
resolution: direct.resolution
|
|
1245
|
+
},
|
|
1246
|
+
graph: {
|
|
1247
|
+
nodeCount: graph.nodes.length,
|
|
1248
|
+
truncated: graph.truncated,
|
|
1249
|
+
maxNodes,
|
|
1250
|
+
maxDepth
|
|
1251
|
+
},
|
|
1252
|
+
assumptions: graph.assumptions,
|
|
1253
|
+
external
|
|
1254
|
+
};
|
|
1255
|
+
};
|
|
1256
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1257
|
+
var MAX_RETRIES2 = 3;
|
|
1258
|
+
var RETRY_BASE_DELAY_MS2 = 500;
|
|
1259
|
+
var round42 = (value) => Number(value.toFixed(4));
|
|
1260
|
+
var parseDate = (iso) => {
|
|
1261
|
+
if (iso === void 0) {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
const value = Date.parse(iso);
|
|
1265
|
+
return Number.isNaN(value) ? null : value;
|
|
1266
|
+
};
|
|
1267
|
+
var NpmRegistryMetadataProvider = class {
|
|
1268
|
+
cache = /* @__PURE__ */ new Map();
|
|
1269
|
+
async fetchWeeklyDownloads(name) {
|
|
1270
|
+
const encodedName = encodeURIComponent(name);
|
|
1271
|
+
const payload = await fetchJsonWithRetry(
|
|
1272
|
+
`https://api.npmjs.org/downloads/point/last-week/${encodedName}`,
|
|
1273
|
+
{ retries: MAX_RETRIES2, baseDelayMs: RETRY_BASE_DELAY_MS2 }
|
|
1274
|
+
);
|
|
1275
|
+
if (payload === null) {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
const downloads = payload.downloads;
|
|
1279
|
+
if (typeof downloads !== "number" || Number.isNaN(downloads) || downloads < 0) {
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
return Math.floor(downloads);
|
|
1283
|
+
}
|
|
1284
|
+
async getMetadata(name, version2, context) {
|
|
1285
|
+
const key = `${name}@${version2}`;
|
|
1286
|
+
if (this.cache.has(key)) {
|
|
1287
|
+
return this.cache.get(key) ?? null;
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
const encodedName = encodeURIComponent(name);
|
|
1291
|
+
const payload = await fetchJsonWithRetry(
|
|
1292
|
+
`https://registry.npmjs.org/${encodedName}`,
|
|
1293
|
+
{ retries: MAX_RETRIES2, baseDelayMs: RETRY_BASE_DELAY_MS2 }
|
|
1294
|
+
);
|
|
1295
|
+
if (payload === null) {
|
|
1296
|
+
this.cache.set(key, null);
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
const timeEntries = payload.time ?? {};
|
|
1300
|
+
const publishDates = Object.entries(timeEntries).filter(([tag]) => tag !== "created" && tag !== "modified").map(([, date]) => parseDate(date)).filter((value) => value !== null).sort((a, b) => a - b);
|
|
1301
|
+
const modifiedAt = parseDate(timeEntries["modified"]);
|
|
1302
|
+
const now = Date.now();
|
|
1303
|
+
const daysSinceLastRelease = modifiedAt === null ? null : Math.max(0, round42((now - modifiedAt) / ONE_DAY_MS));
|
|
1304
|
+
let releaseFrequencyDays = null;
|
|
1305
|
+
if (publishDates.length >= 2) {
|
|
1306
|
+
const totalIntervals = publishDates.length - 1;
|
|
1307
|
+
let sum = 0;
|
|
1308
|
+
for (let i = 1; i < publishDates.length; i += 1) {
|
|
1309
|
+
const current = publishDates[i];
|
|
1310
|
+
const previous = publishDates[i - 1];
|
|
1311
|
+
if (current !== void 0 && previous !== void 0) {
|
|
1312
|
+
sum += current - previous;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
releaseFrequencyDays = round42(sum / totalIntervals / ONE_DAY_MS);
|
|
1316
|
+
}
|
|
1317
|
+
const maintainers = payload.maintainers ?? [];
|
|
1318
|
+
const maintainerCount = maintainers.length > 0 ? maintainers.length : null;
|
|
1319
|
+
const weeklyDownloads = context.directDependency ? await this.fetchWeeklyDownloads(name).catch(() => null) : null;
|
|
1320
|
+
const metadata = {
|
|
1321
|
+
name,
|
|
1322
|
+
version: version2,
|
|
1323
|
+
weeklyDownloads,
|
|
1324
|
+
maintainerCount,
|
|
1325
|
+
releaseFrequencyDays,
|
|
1326
|
+
daysSinceLastRelease,
|
|
1327
|
+
repositoryActivity30d: null,
|
|
1328
|
+
busFactor: null
|
|
1329
|
+
};
|
|
1330
|
+
this.cache.set(key, metadata);
|
|
1331
|
+
return metadata;
|
|
1332
|
+
} catch {
|
|
1333
|
+
this.cache.set(key, null);
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
var NoopMetadataProvider = class {
|
|
1339
|
+
async getMetadata(_name, _version, _context) {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
var analyzeDependencyExposureFromProject = async (input, onProgress) => {
|
|
1344
|
+
const metadataProvider = process.env["CODESENTINEL_EXTERNAL_METADATA"] === "none" ? new NoopMetadataProvider() : new NpmRegistryMetadataProvider();
|
|
1345
|
+
return analyzeDependencyExposure(input, metadataProvider, onProgress);
|
|
1346
|
+
};
|
|
1347
|
+
var analyzeDependencyCandidateFromRegistry = async (input) => {
|
|
1348
|
+
const metadataProvider = process.env["CODESENTINEL_EXTERNAL_METADATA"] === "none" ? new NoopMetadataProvider() : new NpmRegistryMetadataProvider();
|
|
1349
|
+
return analyzeDependencyCandidate(input, metadataProvider);
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
// src/index.ts
|
|
1353
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1354
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
1355
|
+
import { fileURLToPath } from "url";
|
|
1356
|
+
|
|
1357
|
+
// src/application/format-analyze-output.ts
|
|
1358
|
+
var createSummaryShape = (summary) => ({
|
|
1359
|
+
targetPath: summary.structural.targetPath,
|
|
1360
|
+
structural: summary.structural.metrics,
|
|
1361
|
+
evolution: summary.evolution.available ? {
|
|
1362
|
+
available: true,
|
|
1363
|
+
metrics: summary.evolution.metrics,
|
|
1364
|
+
hotspotsTop: summary.evolution.hotspots.slice(0, 5).map((hotspot) => hotspot.filePath)
|
|
1365
|
+
} : {
|
|
1366
|
+
available: false,
|
|
1367
|
+
reason: summary.evolution.reason
|
|
1368
|
+
},
|
|
1369
|
+
external: summary.external.available ? {
|
|
1370
|
+
available: true,
|
|
1371
|
+
metrics: summary.external.metrics,
|
|
1372
|
+
highRiskDependenciesTop: summary.external.highRiskDependencies.slice(0, 10),
|
|
1373
|
+
highRiskDevelopmentDependenciesTop: summary.external.highRiskDevelopmentDependencies.slice(
|
|
1374
|
+
0,
|
|
1375
|
+
10
|
|
1376
|
+
),
|
|
1377
|
+
transitiveExposureDependenciesTop: summary.external.transitiveExposureDependencies.slice(0, 10)
|
|
1378
|
+
} : {
|
|
1379
|
+
available: false,
|
|
1380
|
+
reason: summary.external.reason
|
|
1381
|
+
},
|
|
1382
|
+
risk: {
|
|
1383
|
+
repositoryScore: summary.risk.repositoryScore,
|
|
1384
|
+
normalizedScore: summary.risk.normalizedScore,
|
|
1385
|
+
hotspotsTop: summary.risk.hotspots.slice(0, 5).map((hotspot) => ({
|
|
1386
|
+
file: hotspot.file,
|
|
1387
|
+
score: hotspot.score
|
|
1388
|
+
})),
|
|
1389
|
+
fragileClusterCount: summary.risk.fragileClusters.length,
|
|
1390
|
+
dependencyAmplificationZoneCount: summary.risk.dependencyAmplificationZones.length
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
var formatAnalyzeOutput = (summary, mode) => mode === "json" ? JSON.stringify(summary, null, 2) : JSON.stringify(createSummaryShape(summary), null, 2);
|
|
1394
|
+
|
|
1395
|
+
// src/application/format-dependency-risk-output.ts
|
|
1396
|
+
var createSummaryShape2 = (result) => {
|
|
1397
|
+
if (!result.available) {
|
|
1398
|
+
return {
|
|
1399
|
+
available: false,
|
|
1400
|
+
reason: result.reason,
|
|
1401
|
+
dependency: result.dependency
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
const direct = result.external.dependencies[0];
|
|
1405
|
+
return {
|
|
1406
|
+
available: true,
|
|
1407
|
+
dependency: result.dependency,
|
|
1408
|
+
graph: result.graph,
|
|
1409
|
+
assumptions: result.assumptions,
|
|
1410
|
+
external: {
|
|
1411
|
+
metrics: result.external.metrics,
|
|
1412
|
+
ownRiskSignals: direct?.ownRiskSignals ?? [],
|
|
1413
|
+
inheritedRiskSignals: direct?.inheritedRiskSignals ?? [],
|
|
1414
|
+
highRiskDependenciesTop: result.external.highRiskDependencies.slice(0, 10),
|
|
1415
|
+
transitiveExposureDependenciesTop: result.external.transitiveExposureDependencies.slice(0, 10)
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
};
|
|
1419
|
+
var formatDependencyRiskOutput = (result, mode) => mode === "json" ? JSON.stringify(result, null, 2) : JSON.stringify(createSummaryShape2(result), null, 2);
|
|
1420
|
+
|
|
1421
|
+
// src/application/logger.ts
|
|
1422
|
+
var logLevelRank = {
|
|
1423
|
+
error: 0,
|
|
1424
|
+
warn: 1,
|
|
1425
|
+
info: 2,
|
|
1426
|
+
debug: 3
|
|
1427
|
+
};
|
|
1428
|
+
var noop = () => {
|
|
1429
|
+
};
|
|
1430
|
+
var createSilentLogger = () => ({
|
|
1431
|
+
error: noop,
|
|
1432
|
+
warn: noop,
|
|
1433
|
+
info: noop,
|
|
1434
|
+
debug: noop
|
|
1435
|
+
});
|
|
1436
|
+
var shouldLog = (configuredLevel, messageLevel) => {
|
|
1437
|
+
if (configuredLevel === "silent") {
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
return logLevelRank[messageLevel] <= logLevelRank[configuredLevel];
|
|
1441
|
+
};
|
|
1442
|
+
var write = (messageLevel, message) => {
|
|
1443
|
+
process.stderr.write(`[codesentinel] ${messageLevel.toUpperCase()} ${message}
|
|
1444
|
+
`);
|
|
1445
|
+
};
|
|
1446
|
+
var createStderrLogger = (level) => {
|
|
1447
|
+
if (level === "silent") {
|
|
1448
|
+
return createSilentLogger();
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
error: (message) => {
|
|
1452
|
+
if (shouldLog(level, "error")) {
|
|
1453
|
+
write("error", message);
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
warn: (message) => {
|
|
1457
|
+
if (shouldLog(level, "warn")) {
|
|
1458
|
+
write("warn", message);
|
|
1459
|
+
}
|
|
1460
|
+
},
|
|
1461
|
+
info: (message) => {
|
|
1462
|
+
if (shouldLog(level, "info")) {
|
|
1463
|
+
write("info", message);
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
debug: (message) => {
|
|
1467
|
+
if (shouldLog(level, "debug")) {
|
|
1468
|
+
write("debug", message);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
};
|
|
1473
|
+
var parseLogLevel = (value) => {
|
|
1474
|
+
switch (value) {
|
|
1475
|
+
case "silent":
|
|
1476
|
+
case "error":
|
|
1477
|
+
case "warn":
|
|
1478
|
+
case "info":
|
|
1479
|
+
case "debug":
|
|
1480
|
+
return value;
|
|
1481
|
+
default:
|
|
1482
|
+
return "info";
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
// src/application/run-analyze-command.ts
|
|
1487
|
+
import { resolve as resolve2 } from "path";
|
|
1488
|
+
|
|
1489
|
+
// ../code-graph/dist/index.js
|
|
1490
|
+
import { extname, isAbsolute, relative, resolve } from "path";
|
|
1491
|
+
import * as ts from "typescript";
|
|
1492
|
+
var edgeKey = (from, to) => `${from}\0${to}`;
|
|
1493
|
+
var createGraphData = (nodes, rawEdges) => {
|
|
1494
|
+
const sortedNodes = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
|
|
1495
|
+
const knownNodeIds = new Set(sortedNodes.map((node) => node.id));
|
|
1496
|
+
const uniqueEdgeMap = /* @__PURE__ */ new Map();
|
|
1497
|
+
for (const edge of rawEdges) {
|
|
1498
|
+
if (edge.from === edge.to) {
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
if (!knownNodeIds.has(edge.from) || !knownNodeIds.has(edge.to)) {
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
uniqueEdgeMap.set(edgeKey(edge.from, edge.to), edge);
|
|
1505
|
+
}
|
|
1506
|
+
const sortedEdges = [...uniqueEdgeMap.values()].sort((a, b) => {
|
|
1507
|
+
const fromCompare = a.from.localeCompare(b.from);
|
|
1508
|
+
if (fromCompare !== 0) {
|
|
1509
|
+
return fromCompare;
|
|
1510
|
+
}
|
|
1511
|
+
return a.to.localeCompare(b.to);
|
|
1512
|
+
});
|
|
1513
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1514
|
+
for (const node of sortedNodes) {
|
|
1515
|
+
adjacency.set(node.id, []);
|
|
1516
|
+
}
|
|
1517
|
+
for (const edge of sortedEdges) {
|
|
1518
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
1519
|
+
}
|
|
1520
|
+
const adjacencyById = /* @__PURE__ */ new Map();
|
|
1521
|
+
for (const [nodeId, targets] of adjacency.entries()) {
|
|
1522
|
+
adjacencyById.set(nodeId, [...targets]);
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
nodes: sortedNodes,
|
|
1526
|
+
edges: sortedEdges,
|
|
1527
|
+
adjacencyById
|
|
1528
|
+
};
|
|
1529
|
+
};
|
|
1530
|
+
var runTarjanScc = (adjacencyById) => {
|
|
1531
|
+
let index = 0;
|
|
1532
|
+
const indices = /* @__PURE__ */ new Map();
|
|
1533
|
+
const lowLink = /* @__PURE__ */ new Map();
|
|
1534
|
+
const stack = [];
|
|
1535
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
1536
|
+
const components = [];
|
|
1537
|
+
const strongConnect = (nodeId) => {
|
|
1538
|
+
indices.set(nodeId, index);
|
|
1539
|
+
lowLink.set(nodeId, index);
|
|
1540
|
+
index += 1;
|
|
1541
|
+
stack.push(nodeId);
|
|
1542
|
+
onStack.add(nodeId);
|
|
1543
|
+
const neighbors = adjacencyById.get(nodeId) ?? [];
|
|
1544
|
+
for (const nextId of neighbors) {
|
|
1545
|
+
if (!indices.has(nextId)) {
|
|
1546
|
+
strongConnect(nextId);
|
|
1547
|
+
const nodeLowLink2 = lowLink.get(nodeId);
|
|
1548
|
+
const nextLowLink = lowLink.get(nextId);
|
|
1549
|
+
if (nodeLowLink2 !== void 0 && nextLowLink !== void 0 && nextLowLink < nodeLowLink2) {
|
|
1550
|
+
lowLink.set(nodeId, nextLowLink);
|
|
1551
|
+
}
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
if (onStack.has(nextId)) {
|
|
1555
|
+
const nodeLowLink2 = lowLink.get(nodeId);
|
|
1556
|
+
const nextIndex = indices.get(nextId);
|
|
1557
|
+
if (nodeLowLink2 !== void 0 && nextIndex !== void 0 && nextIndex < nodeLowLink2) {
|
|
1558
|
+
lowLink.set(nodeId, nextIndex);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
const nodeLowLink = lowLink.get(nodeId);
|
|
1563
|
+
const nodeIndex = indices.get(nodeId);
|
|
1564
|
+
if (nodeLowLink === void 0 || nodeIndex === void 0 || nodeLowLink !== nodeIndex) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const component = [];
|
|
1568
|
+
for (; ; ) {
|
|
1569
|
+
const popped = stack.pop();
|
|
1570
|
+
if (popped === void 0) {
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
onStack.delete(popped);
|
|
1574
|
+
component.push(popped);
|
|
1575
|
+
if (popped === nodeId) {
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
component.sort((a, b) => a.localeCompare(b));
|
|
1580
|
+
components.push(component);
|
|
1581
|
+
};
|
|
1582
|
+
const nodeIds = [...adjacencyById.keys()].sort((a, b) => a.localeCompare(b));
|
|
1583
|
+
for (const nodeId of nodeIds) {
|
|
1584
|
+
if (!indices.has(nodeId)) {
|
|
1585
|
+
strongConnect(nodeId);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
components.sort((a, b) => {
|
|
1589
|
+
const firstA = a[0] ?? "";
|
|
1590
|
+
const firstB = b[0] ?? "";
|
|
1591
|
+
return firstA.localeCompare(firstB);
|
|
1592
|
+
});
|
|
1593
|
+
return { components };
|
|
1594
|
+
};
|
|
1595
|
+
var hasSelfLoop = (nodeId, adjacencyById) => {
|
|
1596
|
+
const targets = adjacencyById.get(nodeId) ?? [];
|
|
1597
|
+
return targets.includes(nodeId);
|
|
1598
|
+
};
|
|
1599
|
+
var computeCyclesAndDepth = (graph) => {
|
|
1600
|
+
const { components } = runTarjanScc(graph.adjacencyById);
|
|
1601
|
+
const cycles = [];
|
|
1602
|
+
const componentByNodeId = /* @__PURE__ */ new Map();
|
|
1603
|
+
components.forEach((component, index) => {
|
|
1604
|
+
for (const nodeId of component) {
|
|
1605
|
+
componentByNodeId.set(nodeId, index);
|
|
1606
|
+
}
|
|
1607
|
+
if (component.length > 1) {
|
|
1608
|
+
cycles.push({ nodes: [...component] });
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const onlyNode = component[0];
|
|
1612
|
+
if (onlyNode !== void 0 && hasSelfLoop(onlyNode, graph.adjacencyById)) {
|
|
1613
|
+
cycles.push({ nodes: [...component] });
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
const dagOutgoing = /* @__PURE__ */ new Map();
|
|
1617
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
1618
|
+
for (let i = 0; i < components.length; i += 1) {
|
|
1619
|
+
dagOutgoing.set(i, /* @__PURE__ */ new Set());
|
|
1620
|
+
inDegree.set(i, 0);
|
|
1621
|
+
}
|
|
1622
|
+
for (const edge of graph.edges) {
|
|
1623
|
+
const fromComponent = componentByNodeId.get(edge.from);
|
|
1624
|
+
const toComponent = componentByNodeId.get(edge.to);
|
|
1625
|
+
if (fromComponent === void 0 || toComponent === void 0 || fromComponent === toComponent) {
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
const outgoing = dagOutgoing.get(fromComponent);
|
|
1629
|
+
if (outgoing?.has(toComponent) === true) {
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
outgoing?.add(toComponent);
|
|
1633
|
+
inDegree.set(toComponent, (inDegree.get(toComponent) ?? 0) + 1);
|
|
1634
|
+
}
|
|
1635
|
+
const queue = [];
|
|
1636
|
+
const depthByComponent = /* @__PURE__ */ new Map();
|
|
1637
|
+
for (let i = 0; i < components.length; i += 1) {
|
|
1638
|
+
if ((inDegree.get(i) ?? 0) === 0) {
|
|
1639
|
+
queue.push(i);
|
|
1640
|
+
depthByComponent.set(i, 0);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
let cursor = 0;
|
|
1644
|
+
while (cursor < queue.length) {
|
|
1645
|
+
const componentId = queue[cursor];
|
|
1646
|
+
cursor += 1;
|
|
1647
|
+
if (componentId === void 0) {
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const currentDepth = depthByComponent.get(componentId) ?? 0;
|
|
1651
|
+
const outgoing = dagOutgoing.get(componentId) ?? /* @__PURE__ */ new Set();
|
|
1652
|
+
for (const nextComponent of outgoing) {
|
|
1653
|
+
const nextDepth = depthByComponent.get(nextComponent) ?? 0;
|
|
1654
|
+
if (currentDepth + 1 > nextDepth) {
|
|
1655
|
+
depthByComponent.set(nextComponent, currentDepth + 1);
|
|
1656
|
+
}
|
|
1657
|
+
const remainingIncoming = (inDegree.get(nextComponent) ?? 0) - 1;
|
|
1658
|
+
inDegree.set(nextComponent, remainingIncoming);
|
|
1659
|
+
if (remainingIncoming === 0) {
|
|
1660
|
+
queue.push(nextComponent);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const depthByNodeId = /* @__PURE__ */ new Map();
|
|
1665
|
+
let graphDepth = 0;
|
|
1666
|
+
components.forEach((component, componentId) => {
|
|
1667
|
+
const componentDepth = depthByComponent.get(componentId) ?? 0;
|
|
1668
|
+
if (componentDepth > graphDepth) {
|
|
1669
|
+
graphDepth = componentDepth;
|
|
1670
|
+
}
|
|
1671
|
+
for (const nodeId of component) {
|
|
1672
|
+
depthByNodeId.set(nodeId, componentDepth);
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
cycles.sort((a, b) => {
|
|
1676
|
+
const firstA = a.nodes[0] ?? "";
|
|
1677
|
+
const firstB = b.nodes[0] ?? "";
|
|
1678
|
+
return firstA.localeCompare(firstB);
|
|
1679
|
+
});
|
|
1680
|
+
return {
|
|
1681
|
+
depthByNodeId,
|
|
1682
|
+
graphDepth,
|
|
1683
|
+
cycles
|
|
1684
|
+
};
|
|
1685
|
+
};
|
|
1686
|
+
var createGraphAnalysisSummary = (targetPath, graph) => {
|
|
1687
|
+
const fanInById = /* @__PURE__ */ new Map();
|
|
1688
|
+
const fanOutById = /* @__PURE__ */ new Map();
|
|
1689
|
+
for (const node of graph.nodes) {
|
|
1690
|
+
fanInById.set(node.id, 0);
|
|
1691
|
+
fanOutById.set(node.id, graph.adjacencyById.get(node.id)?.length ?? 0);
|
|
1692
|
+
}
|
|
1693
|
+
for (const edge of graph.edges) {
|
|
1694
|
+
fanInById.set(edge.to, (fanInById.get(edge.to) ?? 0) + 1);
|
|
1695
|
+
}
|
|
1696
|
+
const { cycles, depthByNodeId, graphDepth } = computeCyclesAndDepth(graph);
|
|
1697
|
+
let maxFanIn = 0;
|
|
1698
|
+
let maxFanOut = 0;
|
|
1699
|
+
const files = graph.nodes.map((node) => {
|
|
1700
|
+
const fanIn = fanInById.get(node.id) ?? 0;
|
|
1701
|
+
const fanOut = fanOutById.get(node.id) ?? 0;
|
|
1702
|
+
if (fanIn > maxFanIn) {
|
|
1703
|
+
maxFanIn = fanIn;
|
|
1704
|
+
}
|
|
1705
|
+
if (fanOut > maxFanOut) {
|
|
1706
|
+
maxFanOut = fanOut;
|
|
1707
|
+
}
|
|
1708
|
+
return {
|
|
1709
|
+
id: node.id,
|
|
1710
|
+
relativePath: node.relativePath,
|
|
1711
|
+
directDependencies: graph.adjacencyById.get(node.id) ?? [],
|
|
1712
|
+
fanIn,
|
|
1713
|
+
fanOut,
|
|
1714
|
+
depth: depthByNodeId.get(node.id) ?? 0
|
|
1715
|
+
};
|
|
1716
|
+
});
|
|
1717
|
+
const metrics = {
|
|
1718
|
+
nodeCount: graph.nodes.length,
|
|
1719
|
+
edgeCount: graph.edges.length,
|
|
1720
|
+
cycleCount: cycles.length,
|
|
1721
|
+
graphDepth,
|
|
1722
|
+
maxFanIn,
|
|
1723
|
+
maxFanOut
|
|
1724
|
+
};
|
|
1725
|
+
return {
|
|
1726
|
+
targetPath,
|
|
1727
|
+
nodes: graph.nodes,
|
|
1728
|
+
edges: graph.edges,
|
|
1729
|
+
cycles,
|
|
1730
|
+
files,
|
|
1731
|
+
metrics
|
|
1732
|
+
};
|
|
1733
|
+
};
|
|
1734
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
1735
|
+
var SCAN_EXCLUDES = [
|
|
1736
|
+
"**/node_modules/**",
|
|
1737
|
+
"**/.git/**",
|
|
1738
|
+
"**/dist/**",
|
|
1739
|
+
"**/build/**",
|
|
1740
|
+
"**/.next/**",
|
|
1741
|
+
"**/coverage/**",
|
|
1742
|
+
"**/.turbo/**",
|
|
1743
|
+
"**/.cache/**",
|
|
1744
|
+
"**/out/**"
|
|
1745
|
+
];
|
|
1746
|
+
var SCAN_INCLUDES = ["**/*"];
|
|
1747
|
+
var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1748
|
+
"node_modules",
|
|
1749
|
+
".git",
|
|
1750
|
+
"dist",
|
|
1751
|
+
"build",
|
|
1752
|
+
".next",
|
|
1753
|
+
"coverage",
|
|
1754
|
+
".turbo",
|
|
1755
|
+
".cache",
|
|
1756
|
+
"out"
|
|
1757
|
+
]);
|
|
1758
|
+
var normalizePath = (pathValue) => pathValue.replaceAll("\\", "/");
|
|
1759
|
+
var isProjectSourceFile = (filePath, projectRoot) => {
|
|
1760
|
+
const extension = extname(filePath);
|
|
1761
|
+
if (!SOURCE_EXTENSIONS.has(extension)) {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
const relativePath = relative(projectRoot, filePath);
|
|
1765
|
+
if (relativePath.startsWith("..")) {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
const normalizedRelativePath = normalizePath(relativePath);
|
|
1769
|
+
const segments = normalizedRelativePath.split("/");
|
|
1770
|
+
return !segments.some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
1771
|
+
};
|
|
1772
|
+
var discoverSourceFilesByScan = (projectRoot) => {
|
|
1773
|
+
const files = ts.sys.readDirectory(
|
|
1774
|
+
projectRoot,
|
|
1775
|
+
[...SOURCE_EXTENSIONS],
|
|
1776
|
+
SCAN_EXCLUDES,
|
|
1777
|
+
SCAN_INCLUDES
|
|
1778
|
+
);
|
|
1779
|
+
return files.map((filePath) => resolve(filePath));
|
|
1780
|
+
};
|
|
1781
|
+
var parseTsConfigFile = (configPath) => {
|
|
1782
|
+
const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(
|
|
1783
|
+
configPath,
|
|
1784
|
+
{},
|
|
1785
|
+
{
|
|
1786
|
+
...ts.sys,
|
|
1787
|
+
onUnRecoverableConfigFileDiagnostic: () => {
|
|
1788
|
+
throw new Error(`Failed to parse TypeScript configuration at ${configPath}`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
);
|
|
1792
|
+
if (parsedCommandLine === void 0) {
|
|
1793
|
+
throw new Error(`Failed to parse TypeScript configuration at ${configPath}`);
|
|
1794
|
+
}
|
|
1795
|
+
return parsedCommandLine;
|
|
1796
|
+
};
|
|
1797
|
+
var collectFilesFromTsConfigGraph = (projectRoot) => {
|
|
1798
|
+
const rootConfigPath = ts.findConfigFile(projectRoot, ts.sys.fileExists, "tsconfig.json");
|
|
1799
|
+
if (rootConfigPath === void 0) {
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
const visitedConfigPaths = /* @__PURE__ */ new Set();
|
|
1803
|
+
const collectedFiles = /* @__PURE__ */ new Set();
|
|
1804
|
+
let rootOptions = null;
|
|
1805
|
+
const visitConfig = (configPath) => {
|
|
1806
|
+
const absoluteConfigPath = resolve(configPath);
|
|
1807
|
+
if (visitedConfigPaths.has(absoluteConfigPath)) {
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
visitedConfigPaths.add(absoluteConfigPath);
|
|
1811
|
+
const parsed = parseTsConfigFile(absoluteConfigPath);
|
|
1812
|
+
if (rootOptions === null) {
|
|
1813
|
+
rootOptions = parsed.options;
|
|
1814
|
+
}
|
|
1815
|
+
for (const filePath of parsed.fileNames) {
|
|
1816
|
+
collectedFiles.add(resolve(filePath));
|
|
1817
|
+
}
|
|
1818
|
+
for (const reference of parsed.projectReferences ?? []) {
|
|
1819
|
+
const referencePath = resolve(reference.path);
|
|
1820
|
+
const referenceConfigPath = ts.sys.directoryExists(referencePath) ? ts.findConfigFile(referencePath, ts.sys.fileExists, "tsconfig.json") : referencePath;
|
|
1821
|
+
if (referenceConfigPath !== void 0 && ts.sys.fileExists(referenceConfigPath)) {
|
|
1822
|
+
visitConfig(referenceConfigPath);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
visitConfig(rootConfigPath);
|
|
1827
|
+
return {
|
|
1828
|
+
fileNames: [...collectedFiles],
|
|
1829
|
+
rootOptions: rootOptions ?? {
|
|
1830
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext
|
|
1831
|
+
},
|
|
1832
|
+
visitedConfigCount: visitedConfigPaths.size
|
|
1833
|
+
};
|
|
1834
|
+
};
|
|
1835
|
+
var createCompilerOptions = (base) => ({
|
|
1836
|
+
...base,
|
|
1837
|
+
allowJs: true,
|
|
1838
|
+
moduleResolution: base?.moduleResolution ?? ts.ModuleResolutionKind.NodeNext
|
|
1839
|
+
});
|
|
1840
|
+
var parseTsConfig = (projectRoot) => {
|
|
1841
|
+
const collected = collectFilesFromTsConfigGraph(projectRoot);
|
|
1842
|
+
if (collected === null) {
|
|
1843
|
+
return {
|
|
1844
|
+
fileNames: discoverSourceFilesByScan(projectRoot),
|
|
1845
|
+
options: createCompilerOptions(void 0),
|
|
1846
|
+
tsconfigCount: 0,
|
|
1847
|
+
usedFallbackScan: true
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
if (collected.fileNames.length === 0) {
|
|
1851
|
+
return {
|
|
1852
|
+
fileNames: discoverSourceFilesByScan(projectRoot),
|
|
1853
|
+
options: createCompilerOptions(collected.rootOptions),
|
|
1854
|
+
tsconfigCount: collected.visitedConfigCount,
|
|
1855
|
+
usedFallbackScan: true
|
|
1274
1856
|
};
|
|
1275
1857
|
}
|
|
1858
|
+
return {
|
|
1859
|
+
fileNames: collected.fileNames,
|
|
1860
|
+
options: createCompilerOptions(collected.rootOptions),
|
|
1861
|
+
tsconfigCount: collected.visitedConfigCount,
|
|
1862
|
+
usedFallbackScan: false
|
|
1863
|
+
};
|
|
1276
1864
|
};
|
|
1277
|
-
var
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
if (iso === void 0) {
|
|
1281
|
-
return null;
|
|
1865
|
+
var getSpecifierFromExpression = (expression) => {
|
|
1866
|
+
if (ts.isStringLiteral(expression)) {
|
|
1867
|
+
return expression.text;
|
|
1282
1868
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1869
|
+
if (ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
1870
|
+
return expression.text;
|
|
1871
|
+
}
|
|
1872
|
+
return void 0;
|
|
1285
1873
|
};
|
|
1286
|
-
var
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1874
|
+
var hasRuntimeImport = (importDeclaration) => {
|
|
1875
|
+
const importClause = importDeclaration.importClause;
|
|
1876
|
+
if (importClause === void 0) {
|
|
1877
|
+
return true;
|
|
1878
|
+
}
|
|
1879
|
+
if (importClause.isTypeOnly) {
|
|
1880
|
+
return false;
|
|
1881
|
+
}
|
|
1882
|
+
if (importClause.name !== void 0) {
|
|
1883
|
+
return true;
|
|
1884
|
+
}
|
|
1885
|
+
const namedBindings = importClause.namedBindings;
|
|
1886
|
+
if (namedBindings === void 0) {
|
|
1887
|
+
return false;
|
|
1888
|
+
}
|
|
1889
|
+
if (ts.isNamespaceImport(namedBindings)) {
|
|
1890
|
+
return true;
|
|
1891
|
+
}
|
|
1892
|
+
if (namedBindings.elements.length === 0) {
|
|
1893
|
+
return true;
|
|
1894
|
+
}
|
|
1895
|
+
return namedBindings.elements.some((element) => !element.isTypeOnly);
|
|
1896
|
+
};
|
|
1897
|
+
var extractModuleSpecifiers = (sourceFile) => {
|
|
1898
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
1899
|
+
const visit = (node) => {
|
|
1900
|
+
if (ts.isImportDeclaration(node)) {
|
|
1901
|
+
if (hasRuntimeImport(node) && node.moduleSpecifier !== void 0) {
|
|
1902
|
+
const specifier = getSpecifierFromExpression(node.moduleSpecifier);
|
|
1903
|
+
if (specifier !== void 0) {
|
|
1904
|
+
specifiers.add(specifier);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return;
|
|
1292
1908
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1909
|
+
if (ts.isExportDeclaration(node)) {
|
|
1910
|
+
if (!node.isTypeOnly && node.moduleSpecifier !== void 0) {
|
|
1911
|
+
const specifier = getSpecifierFromExpression(node.moduleSpecifier);
|
|
1912
|
+
if (specifier !== void 0) {
|
|
1913
|
+
specifiers.add(specifier);
|
|
1914
|
+
}
|
|
1299
1915
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
if (ts.isCallExpression(node)) {
|
|
1919
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
|
|
1920
|
+
const firstArgument = node.arguments[0];
|
|
1921
|
+
if (firstArgument !== void 0) {
|
|
1922
|
+
const specifier = getSpecifierFromExpression(firstArgument);
|
|
1923
|
+
if (specifier !== void 0) {
|
|
1924
|
+
specifiers.add(specifier);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length > 0) {
|
|
1929
|
+
const firstArgument = node.arguments[0];
|
|
1930
|
+
if (firstArgument !== void 0) {
|
|
1931
|
+
const specifier = getSpecifierFromExpression(firstArgument);
|
|
1932
|
+
if (specifier !== void 0) {
|
|
1933
|
+
specifiers.add(specifier);
|
|
1315
1934
|
}
|
|
1316
1935
|
}
|
|
1317
|
-
releaseFrequencyDays = round42(sum / totalIntervals / ONE_DAY_MS);
|
|
1318
1936
|
}
|
|
1319
|
-
const maintainers = payload.maintainers ?? [];
|
|
1320
|
-
const maintainerCount = maintainers.length > 0 ? maintainers.length : null;
|
|
1321
|
-
const metadata = {
|
|
1322
|
-
name,
|
|
1323
|
-
version: version2,
|
|
1324
|
-
maintainerCount,
|
|
1325
|
-
releaseFrequencyDays,
|
|
1326
|
-
daysSinceLastRelease,
|
|
1327
|
-
repositoryActivity30d: null,
|
|
1328
|
-
busFactor: null
|
|
1329
|
-
};
|
|
1330
|
-
this.cache.set(key, metadata);
|
|
1331
|
-
return metadata;
|
|
1332
|
-
} catch {
|
|
1333
|
-
this.cache.set(key, null);
|
|
1334
|
-
return null;
|
|
1335
1937
|
}
|
|
1336
|
-
|
|
1938
|
+
ts.forEachChild(node, visit);
|
|
1939
|
+
};
|
|
1940
|
+
visit(sourceFile);
|
|
1941
|
+
return [...specifiers];
|
|
1337
1942
|
};
|
|
1338
|
-
var
|
|
1339
|
-
|
|
1340
|
-
|
|
1943
|
+
var parseTypescriptProject = (projectPath, onProgress) => {
|
|
1944
|
+
const projectRoot = isAbsolute(projectPath) ? projectPath : resolve(projectPath);
|
|
1945
|
+
const { fileNames, options, tsconfigCount, usedFallbackScan } = parseTsConfig(projectRoot);
|
|
1946
|
+
onProgress?.({ stage: "config_resolved", tsconfigCount, usedFallbackScan });
|
|
1947
|
+
const sourceFilePaths = fileNames.filter((filePath) => isProjectSourceFile(filePath, projectRoot)).map((filePath) => normalizePath(resolve(filePath)));
|
|
1948
|
+
const uniqueSourceFilePaths = [...new Set(sourceFilePaths)].sort((a, b) => a.localeCompare(b));
|
|
1949
|
+
const sourceFilePathSet = new Set(uniqueSourceFilePaths);
|
|
1950
|
+
onProgress?.({ stage: "files_discovered", totalSourceFiles: uniqueSourceFilePaths.length });
|
|
1951
|
+
const program2 = ts.createProgram({
|
|
1952
|
+
rootNames: uniqueSourceFilePaths,
|
|
1953
|
+
options
|
|
1954
|
+
});
|
|
1955
|
+
onProgress?.({ stage: "program_created", totalSourceFiles: uniqueSourceFilePaths.length });
|
|
1956
|
+
const nodeByAbsolutePath = /* @__PURE__ */ new Map();
|
|
1957
|
+
for (const sourcePath of uniqueSourceFilePaths) {
|
|
1958
|
+
const relativePath = normalizePath(relative(projectRoot, sourcePath));
|
|
1959
|
+
const nodeId = relativePath;
|
|
1960
|
+
nodeByAbsolutePath.set(sourcePath, {
|
|
1961
|
+
id: nodeId,
|
|
1962
|
+
absolutePath: sourcePath,
|
|
1963
|
+
relativePath
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
const resolverCache = /* @__PURE__ */ new Map();
|
|
1967
|
+
const edges = [];
|
|
1968
|
+
for (const [index, sourcePath] of uniqueSourceFilePaths.entries()) {
|
|
1969
|
+
const sourceFile = program2.getSourceFile(sourcePath);
|
|
1970
|
+
if (sourceFile === void 0) {
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
const fromNode = nodeByAbsolutePath.get(sourcePath);
|
|
1974
|
+
if (fromNode === void 0) {
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
const moduleSpecifiers = extractModuleSpecifiers(sourceFile);
|
|
1978
|
+
for (const specifier of moduleSpecifiers) {
|
|
1979
|
+
const cacheKey = `${sourcePath}\0${specifier}`;
|
|
1980
|
+
let resolvedPath = resolverCache.get(cacheKey);
|
|
1981
|
+
if (resolvedPath === void 0 && !resolverCache.has(cacheKey)) {
|
|
1982
|
+
const resolved = ts.resolveModuleName(specifier, sourcePath, options, ts.sys).resolvedModule;
|
|
1983
|
+
if (resolved !== void 0) {
|
|
1984
|
+
resolvedPath = normalizePath(resolve(resolved.resolvedFileName));
|
|
1985
|
+
}
|
|
1986
|
+
resolverCache.set(cacheKey, resolvedPath);
|
|
1987
|
+
}
|
|
1988
|
+
if (resolvedPath === void 0 || !sourceFilePathSet.has(resolvedPath)) {
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
const toNode = nodeByAbsolutePath.get(resolvedPath);
|
|
1992
|
+
if (toNode === void 0) {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
edges.push({ from: fromNode.id, to: toNode.id });
|
|
1996
|
+
}
|
|
1997
|
+
const processed = index + 1;
|
|
1998
|
+
if (processed === 1 || processed === uniqueSourceFilePaths.length || processed % 50 === 0) {
|
|
1999
|
+
onProgress?.({
|
|
2000
|
+
stage: "file_processed",
|
|
2001
|
+
processed,
|
|
2002
|
+
total: uniqueSourceFilePaths.length,
|
|
2003
|
+
filePath: fromNode.id
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
1341
2006
|
}
|
|
2007
|
+
onProgress?.({ stage: "edges_resolved", totalEdges: edges.length });
|
|
2008
|
+
return {
|
|
2009
|
+
nodes: [...nodeByAbsolutePath.values()],
|
|
2010
|
+
edges
|
|
2011
|
+
};
|
|
1342
2012
|
};
|
|
1343
|
-
var
|
|
1344
|
-
const
|
|
1345
|
-
|
|
2013
|
+
var buildProjectGraphSummary = (input) => {
|
|
2014
|
+
const parsedProject = parseTypescriptProject(input.projectPath, input.onProgress);
|
|
2015
|
+
const graphData = createGraphData(parsedProject.nodes, parsedProject.edges);
|
|
2016
|
+
return createGraphAnalysisSummary(input.projectPath, graphData);
|
|
1346
2017
|
};
|
|
1347
2018
|
|
|
1348
2019
|
// ../git-analyzer/dist/index.js
|
|
@@ -1899,25 +2570,18 @@ var DEFAULT_RISK_ENGINE_CONFIG = {
|
|
|
1899
2570
|
inheritedSignalMultiplier: 0.45,
|
|
1900
2571
|
// At this age, staleness reaches 50% risk.
|
|
1901
2572
|
abandonedHalfLifeDays: 540,
|
|
1902
|
-
missingMetadataPenalty: 0.5
|
|
2573
|
+
missingMetadataPenalty: 0.5,
|
|
2574
|
+
// At this download volume, popularity reaches 50% of its dampening effect.
|
|
2575
|
+
popularityHalfLifeDownloads: 1e5,
|
|
2576
|
+
// Popularity can only reduce dependency risk by this fraction.
|
|
2577
|
+
popularityMaxDampening: 0.12
|
|
1903
2578
|
},
|
|
1904
2579
|
externalDimension: {
|
|
1905
2580
|
topDependencyPercentile: 0.85,
|
|
1906
2581
|
dependencyDepthHalfLife: 6
|
|
1907
2582
|
}
|
|
1908
2583
|
};
|
|
1909
|
-
var
|
|
1910
|
-
if (Number.isNaN(value)) {
|
|
1911
|
-
return 0;
|
|
1912
|
-
}
|
|
1913
|
-
if (value <= 0) {
|
|
1914
|
-
return 0;
|
|
1915
|
-
}
|
|
1916
|
-
if (value >= 1) {
|
|
1917
|
-
return 1;
|
|
1918
|
-
}
|
|
1919
|
-
return value;
|
|
1920
|
-
};
|
|
2584
|
+
var toUnitInterval = (value) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
1921
2585
|
var round44 = (value) => Number(value.toFixed(4));
|
|
1922
2586
|
var average = (values) => {
|
|
1923
2587
|
if (values.length === 0) {
|
|
@@ -1934,7 +2598,7 @@ var percentile = (values, p) => {
|
|
|
1934
2598
|
return values[0] ?? 0;
|
|
1935
2599
|
}
|
|
1936
2600
|
const sorted = [...values].sort((a, b) => a - b);
|
|
1937
|
-
const position =
|
|
2601
|
+
const position = toUnitInterval(p) * (sorted.length - 1);
|
|
1938
2602
|
const lowerIndex = Math.floor(position);
|
|
1939
2603
|
const upperIndex = Math.ceil(position);
|
|
1940
2604
|
const lower = sorted[lowerIndex] ?? 0;
|
|
@@ -1946,18 +2610,18 @@ var percentile = (values, p) => {
|
|
|
1946
2610
|
return lower + (upper - lower) * ratio;
|
|
1947
2611
|
};
|
|
1948
2612
|
var saturatingComposite = (baseline, amplifications) => {
|
|
1949
|
-
let value =
|
|
2613
|
+
let value = toUnitInterval(baseline);
|
|
1950
2614
|
for (const amplification of amplifications) {
|
|
1951
|
-
const boundedAmplification =
|
|
2615
|
+
const boundedAmplification = toUnitInterval(amplification);
|
|
1952
2616
|
value += (1 - value) * boundedAmplification;
|
|
1953
2617
|
}
|
|
1954
|
-
return
|
|
2618
|
+
return toUnitInterval(value);
|
|
1955
2619
|
};
|
|
1956
2620
|
var halfLifeRisk = (value, halfLife) => {
|
|
1957
2621
|
if (value <= 0 || halfLife <= 0) {
|
|
1958
2622
|
return 0;
|
|
1959
2623
|
}
|
|
1960
|
-
return
|
|
2624
|
+
return toUnitInterval(value / (value + halfLife));
|
|
1961
2625
|
};
|
|
1962
2626
|
var normalizeWeights = (weights, enabled) => {
|
|
1963
2627
|
let total = 0;
|
|
@@ -2004,7 +2668,7 @@ var normalizeWithScale = (value, scale) => {
|
|
|
2004
2668
|
if (scale.upper <= scale.lower) {
|
|
2005
2669
|
return value > 0 ? 1 : 0;
|
|
2006
2670
|
}
|
|
2007
|
-
return
|
|
2671
|
+
return toUnitInterval((value - scale.lower) / (scale.upper - scale.lower));
|
|
2008
2672
|
};
|
|
2009
2673
|
var normalizePath2 = (path) => path.replaceAll("\\", "/");
|
|
2010
2674
|
var dependencySignalWeights = {
|
|
@@ -2030,7 +2694,7 @@ var computeDependencySignalScore = (ownSignals, inheritedSignals, inheritedSigna
|
|
|
2030
2694
|
if (maxWeightedTotal <= 0) {
|
|
2031
2695
|
return 0;
|
|
2032
2696
|
}
|
|
2033
|
-
return
|
|
2697
|
+
return toUnitInterval(weightedTotal / maxWeightedTotal);
|
|
2034
2698
|
};
|
|
2035
2699
|
var computeDependencyScores = (external, config) => {
|
|
2036
2700
|
if (!external.available) {
|
|
@@ -2065,7 +2729,7 @@ var computeDependencyScores = (external, config) => {
|
|
|
2065
2729
|
dependency.inheritedRiskSignals,
|
|
2066
2730
|
config.dependencySignals.inheritedSignalMultiplier
|
|
2067
2731
|
);
|
|
2068
|
-
const maintainerConcentrationRisk = dependency.maintainerCount === null ? config.dependencySignals.missingMetadataPenalty :
|
|
2732
|
+
const maintainerConcentrationRisk = dependency.maintainerCount === null ? config.dependencySignals.missingMetadataPenalty : toUnitInterval(1 / Math.max(1, dependency.maintainerCount));
|
|
2069
2733
|
const stalenessRisk = dependency.daysSinceLastRelease === null ? config.dependencySignals.missingMetadataPenalty : halfLifeRisk(
|
|
2070
2734
|
dependency.daysSinceLastRelease,
|
|
2071
2735
|
config.dependencySignals.abandonedHalfLifeDays
|
|
@@ -2076,11 +2740,17 @@ var computeDependencyScores = (external, config) => {
|
|
|
2076
2740
|
);
|
|
2077
2741
|
const centralityRisk = normalizeWithScale(logScale(dependency.dependents), dependentScale);
|
|
2078
2742
|
const chainDepthRisk = normalizeWithScale(dependency.dependencyDepth, chainDepthScale);
|
|
2079
|
-
const busFactorRisk = dependency.busFactor === null ? config.dependencySignals.missingMetadataPenalty :
|
|
2743
|
+
const busFactorRisk = dependency.busFactor === null ? config.dependencySignals.missingMetadataPenalty : toUnitInterval(1 / Math.max(1, dependency.busFactor));
|
|
2080
2744
|
const weights = config.dependencyFactorWeights;
|
|
2081
|
-
const
|
|
2745
|
+
const baseScore = toUnitInterval(
|
|
2082
2746
|
signalScore * weights.signals + stalenessRisk * weights.staleness + maintainerConcentrationRisk * weights.maintainerConcentration + transitiveBurdenRisk * weights.transitiveBurden + centralityRisk * weights.centrality + chainDepthRisk * weights.chainDepth + busFactorRisk * weights.busFactorRisk
|
|
2083
2747
|
);
|
|
2748
|
+
const hasHardRiskSignal = dependency.ownRiskSignals.includes("abandoned") || dependency.ownRiskSignals.includes("metadata_unavailable") || dependency.ownRiskSignals.includes("single_maintainer");
|
|
2749
|
+
const popularityDampener = dependency.weeklyDownloads === null || hasHardRiskSignal ? 1 : 1 - halfLifeRisk(
|
|
2750
|
+
dependency.weeklyDownloads,
|
|
2751
|
+
config.dependencySignals.popularityHalfLifeDownloads
|
|
2752
|
+
) * config.dependencySignals.popularityMaxDampening;
|
|
2753
|
+
const normalizedScore = toUnitInterval(baseScore * popularityDampener);
|
|
2084
2754
|
return {
|
|
2085
2755
|
dependency: dependency.name,
|
|
2086
2756
|
score: round44(normalizedScore * 100),
|
|
@@ -2098,7 +2768,7 @@ var computeDependencyScores = (external, config) => {
|
|
|
2098
2768
|
external.metrics.dependencyDepth,
|
|
2099
2769
|
config.externalDimension.dependencyDepthHalfLife
|
|
2100
2770
|
);
|
|
2101
|
-
const repositoryExternalPressure =
|
|
2771
|
+
const repositoryExternalPressure = toUnitInterval(
|
|
2102
2772
|
highDependencyRisk * 0.5 + averageDependencyRisk * 0.3 + depthRisk * 0.2
|
|
2103
2773
|
);
|
|
2104
2774
|
return {
|
|
@@ -2166,8 +2836,8 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
|
|
|
2166
2836
|
const averageRisk = average(
|
|
2167
2837
|
files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
|
|
2168
2838
|
);
|
|
2169
|
-
const cycleSizeRisk =
|
|
2170
|
-
const score = round44(
|
|
2839
|
+
const cycleSizeRisk = toUnitInterval((files.length - 1) / 5);
|
|
2840
|
+
const score = round44(toUnitInterval(averageRisk * 0.75 + cycleSizeRisk * 0.25) * 100);
|
|
2171
2841
|
cycleClusterCount += 1;
|
|
2172
2842
|
clusters.push({
|
|
2173
2843
|
id: `cycle:${cycleClusterCount}`,
|
|
@@ -2241,7 +2911,7 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
|
|
|
2241
2911
|
files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
|
|
2242
2912
|
);
|
|
2243
2913
|
const meanCoupling = average(componentPairs.map((pair) => pair.couplingScore));
|
|
2244
|
-
const score = round44(
|
|
2914
|
+
const score = round44(toUnitInterval(meanFileRisk * 0.65 + meanCoupling * 0.35) * 100);
|
|
2245
2915
|
couplingClusterCount += 1;
|
|
2246
2916
|
clusters.push({
|
|
2247
2917
|
id: `coupling:${couplingClusterCount}`,
|
|
@@ -2289,10 +2959,10 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2289
2959
|
const fanOutRisk = normalizeWithScale(logScale(file.fanOut), fanOutScale);
|
|
2290
2960
|
const depthRisk = normalizeWithScale(file.depth, depthScale);
|
|
2291
2961
|
const structuralWeights = config.structuralFactorWeights;
|
|
2292
|
-
const structuralFactor =
|
|
2962
|
+
const structuralFactor = toUnitInterval(
|
|
2293
2963
|
fanInRisk * structuralWeights.fanIn + fanOutRisk * structuralWeights.fanOut + depthRisk * structuralWeights.depth + inCycle * structuralWeights.cycleParticipation
|
|
2294
2964
|
);
|
|
2295
|
-
const structuralCentrality =
|
|
2965
|
+
const structuralCentrality = toUnitInterval((fanInRisk + fanOutRisk) / 2);
|
|
2296
2966
|
let evolutionFactor = 0;
|
|
2297
2967
|
const evolutionMetrics = evolutionByFile.get(filePath);
|
|
2298
2968
|
if (evolution.available && evolutionMetrics !== void 0) {
|
|
@@ -2304,16 +2974,16 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2304
2974
|
logScale(evolutionMetrics.churnTotal),
|
|
2305
2975
|
evolutionScales.churnTotal
|
|
2306
2976
|
);
|
|
2307
|
-
const volatilityRisk =
|
|
2308
|
-
const ownershipConcentrationRisk =
|
|
2309
|
-
const busFactorRisk =
|
|
2977
|
+
const volatilityRisk = toUnitInterval(evolutionMetrics.recentVolatility);
|
|
2978
|
+
const ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShare);
|
|
2979
|
+
const busFactorRisk = toUnitInterval(1 - normalizeWithScale(evolutionMetrics.busFactor, evolutionScales.busFactor));
|
|
2310
2980
|
const evolutionWeights = config.evolutionFactorWeights;
|
|
2311
|
-
evolutionFactor =
|
|
2981
|
+
evolutionFactor = toUnitInterval(
|
|
2312
2982
|
frequencyRisk * evolutionWeights.frequency + churnRisk * evolutionWeights.churn + volatilityRisk * evolutionWeights.recentVolatility + ownershipConcentrationRisk * evolutionWeights.ownershipConcentration + busFactorRisk * evolutionWeights.busFactorRisk
|
|
2313
2983
|
);
|
|
2314
2984
|
}
|
|
2315
|
-
const dependencyAffinity =
|
|
2316
|
-
const externalFactor = external.available ?
|
|
2985
|
+
const dependencyAffinity = toUnitInterval(structuralCentrality * 0.6 + evolutionFactor * 0.4);
|
|
2986
|
+
const externalFactor = external.available ? toUnitInterval(dependencyComputation.repositoryExternalPressure * dependencyAffinity) : 0;
|
|
2317
2987
|
const baseline = structuralFactor * dimensionWeights.structural + evolutionFactor * dimensionWeights.evolution + externalFactor * dimensionWeights.external;
|
|
2318
2988
|
const interactions = [
|
|
2319
2989
|
structuralFactor * evolutionFactor * config.interactionWeights.structuralEvolution,
|
|
@@ -2359,7 +3029,7 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2359
3029
|
const moduleScores = [...moduleFiles.entries()].map(([module, values]) => {
|
|
2360
3030
|
const averageScore = average(values);
|
|
2361
3031
|
const peakScore = values.reduce((max, value) => Math.max(max, value), 0);
|
|
2362
|
-
const normalizedScore =
|
|
3032
|
+
const normalizedScore = toUnitInterval(averageScore * 0.65 + peakScore * 0.35);
|
|
2363
3033
|
return {
|
|
2364
3034
|
module,
|
|
2365
3035
|
score: round44(normalizedScore * 100),
|
|
@@ -2374,10 +3044,10 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2374
3044
|
percentile(externalPressures, config.amplificationZone.percentileThreshold)
|
|
2375
3045
|
);
|
|
2376
3046
|
const dependencyAmplificationZones = fileScores.map((fileScore) => {
|
|
2377
|
-
const intensity =
|
|
3047
|
+
const intensity = toUnitInterval(
|
|
2378
3048
|
fileScore.factors.external * Math.max(fileScore.factors.structural, fileScore.factors.evolution)
|
|
2379
3049
|
);
|
|
2380
|
-
const normalizedZoneScore =
|
|
3050
|
+
const normalizedZoneScore = toUnitInterval(intensity * 0.7 + fileScore.normalizedScore * 0.3);
|
|
2381
3051
|
return {
|
|
2382
3052
|
file: fileScore.file,
|
|
2383
3053
|
score: round44(normalizedZoneScore * 100),
|
|
@@ -2398,7 +3068,7 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2398
3068
|
);
|
|
2399
3069
|
const dependencyAmplification = average(
|
|
2400
3070
|
dependencyAmplificationZones.map(
|
|
2401
|
-
(zone) =>
|
|
3071
|
+
(zone) => toUnitInterval(zone.externalPressure * zone.score / 100)
|
|
2402
3072
|
)
|
|
2403
3073
|
);
|
|
2404
3074
|
const repositoryBaseline = structuralDimension * dimensionWeights.structural + evolutionDimension * dimensionWeights.evolution + externalDimension * dimensionWeights.external;
|
|
@@ -2670,6 +3340,39 @@ program.command("analyze").argument("[path]", "path to the project to analyze").
|
|
|
2670
3340
|
`);
|
|
2671
3341
|
}
|
|
2672
3342
|
);
|
|
3343
|
+
program.command("dependency-risk").argument("<dependency>", "dependency spec to evaluate (for example: react or react@19.0.0)").addOption(
|
|
3344
|
+
new Option(
|
|
3345
|
+
"--log-level <level>",
|
|
3346
|
+
"log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
|
|
3347
|
+
).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
|
|
3348
|
+
).addOption(
|
|
3349
|
+
new Option(
|
|
3350
|
+
"--output <mode>",
|
|
3351
|
+
"output mode: summary (default) or json (full analysis object)"
|
|
3352
|
+
).choices(["summary", "json"]).default("summary")
|
|
3353
|
+
).option("--json", "shortcut for --output json").option("--max-nodes <count>", "maximum dependency nodes to resolve", "250").option("--max-depth <count>", "maximum dependency depth to traverse", "6").action(
|
|
3354
|
+
async (dependency, options) => {
|
|
3355
|
+
const logger = createStderrLogger(options.logLevel);
|
|
3356
|
+
const maxNodes = Number.parseInt(options.maxNodes, 10);
|
|
3357
|
+
const maxDepth = Number.parseInt(options.maxDepth, 10);
|
|
3358
|
+
logger.info(`analyzing dependency candidate: ${dependency}`);
|
|
3359
|
+
const result = await analyzeDependencyCandidateFromRegistry({
|
|
3360
|
+
dependency,
|
|
3361
|
+
maxNodes: Number.isFinite(maxNodes) ? maxNodes : 250,
|
|
3362
|
+
maxDepth: Number.isFinite(maxDepth) ? maxDepth : 6
|
|
3363
|
+
});
|
|
3364
|
+
if (result.available) {
|
|
3365
|
+
logger.info(
|
|
3366
|
+
`dependency analysis completed (${result.dependency.name}@${result.dependency.resolvedVersion})`
|
|
3367
|
+
);
|
|
3368
|
+
} else {
|
|
3369
|
+
logger.warn(`dependency analysis unavailable: ${result.reason}`);
|
|
3370
|
+
}
|
|
3371
|
+
const outputMode = options.json === true ? "json" : options.output;
|
|
3372
|
+
process.stdout.write(`${formatDependencyRiskOutput(result, outputMode)}
|
|
3373
|
+
`);
|
|
3374
|
+
}
|
|
3375
|
+
);
|
|
2673
3376
|
if (process.argv.length <= 2) {
|
|
2674
3377
|
program.outputHelp();
|
|
2675
3378
|
process.exit(0);
|