@getcodesentinel/codesentinel 1.3.1

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/dist/index.js ADDED
@@ -0,0 +1,2685 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
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
+
9
+ // src/application/format-analyze-output.ts
10
+ var createSummaryShape = (summary) => ({
11
+ targetPath: summary.structural.targetPath,
12
+ structural: summary.structural.metrics,
13
+ evolution: summary.evolution.available ? {
14
+ available: true,
15
+ metrics: summary.evolution.metrics,
16
+ hotspotsTop: summary.evolution.hotspots.slice(0, 5).map((hotspot) => hotspot.filePath)
17
+ } : {
18
+ available: false,
19
+ reason: summary.evolution.reason
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
38
+ }
39
+ });
40
+ var formatAnalyzeOutput = (summary, mode) => mode === "json" ? JSON.stringify(summary, null, 2) : JSON.stringify(createSummaryShape(summary), null, 2);
41
+
42
+ // src/application/logger.ts
43
+ var logLevelRank = {
44
+ error: 0,
45
+ warn: 1,
46
+ info: 2,
47
+ debug: 3
48
+ };
49
+ var noop = () => {
50
+ };
51
+ var createSilentLogger = () => ({
52
+ error: noop,
53
+ warn: noop,
54
+ info: noop,
55
+ debug: noop
56
+ });
57
+ var shouldLog = (configuredLevel, messageLevel) => {
58
+ if (configuredLevel === "silent") {
59
+ return false;
60
+ }
61
+ return logLevelRank[messageLevel] <= logLevelRank[configuredLevel];
62
+ };
63
+ var write = (messageLevel, message) => {
64
+ process.stderr.write(`[codesentinel] ${messageLevel.toUpperCase()} ${message}
65
+ `);
66
+ };
67
+ var createStderrLogger = (level) => {
68
+ if (level === "silent") {
69
+ return createSilentLogger();
70
+ }
71
+ return {
72
+ error: (message) => {
73
+ if (shouldLog(level, "error")) {
74
+ write("error", message);
75
+ }
76
+ },
77
+ warn: (message) => {
78
+ if (shouldLog(level, "warn")) {
79
+ write("warn", message);
80
+ }
81
+ },
82
+ info: (message) => {
83
+ if (shouldLog(level, "info")) {
84
+ write("info", message);
85
+ }
86
+ },
87
+ debug: (message) => {
88
+ if (shouldLog(level, "debug")) {
89
+ write("debug", message);
90
+ }
91
+ }
92
+ };
93
+ };
94
+ var parseLogLevel = (value) => {
95
+ switch (value) {
96
+ case "silent":
97
+ case "error":
98
+ case "warn":
99
+ case "info":
100
+ case "debug":
101
+ return value;
102
+ default:
103
+ return "info";
104
+ }
105
+ };
106
+
107
+ // src/application/run-analyze-command.ts
108
+ import { resolve as resolve2 } from "path";
109
+
110
+ // ../code-graph/dist/index.js
111
+ import { extname, isAbsolute, relative, resolve } from "path";
112
+ import * as ts from "typescript";
113
+ var edgeKey = (from, to) => `${from}\0${to}`;
114
+ var createGraphData = (nodes, rawEdges) => {
115
+ const sortedNodes = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
116
+ const knownNodeIds = new Set(sortedNodes.map((node) => node.id));
117
+ const uniqueEdgeMap = /* @__PURE__ */ new Map();
118
+ for (const edge of rawEdges) {
119
+ if (edge.from === edge.to) {
120
+ continue;
121
+ }
122
+ if (!knownNodeIds.has(edge.from) || !knownNodeIds.has(edge.to)) {
123
+ continue;
124
+ }
125
+ uniqueEdgeMap.set(edgeKey(edge.from, edge.to), edge);
126
+ }
127
+ const sortedEdges = [...uniqueEdgeMap.values()].sort((a, b) => {
128
+ const fromCompare = a.from.localeCompare(b.from);
129
+ if (fromCompare !== 0) {
130
+ return fromCompare;
131
+ }
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
+ }
145
+ return {
146
+ nodes: sortedNodes,
147
+ edges: sortedEdges,
148
+ adjacencyById
149
+ };
150
+ };
151
+ var runTarjanScc = (adjacencyById) => {
152
+ let index = 0;
153
+ const indices = /* @__PURE__ */ new Map();
154
+ const lowLink = /* @__PURE__ */ new Map();
155
+ const stack = [];
156
+ const onStack = /* @__PURE__ */ new Set();
157
+ const components = [];
158
+ const strongConnect = (nodeId) => {
159
+ indices.set(nodeId, index);
160
+ lowLink.set(nodeId, index);
161
+ index += 1;
162
+ stack.push(nodeId);
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
+ }
173
+ continue;
174
+ }
175
+ if (onStack.has(nextId)) {
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);
207
+ }
208
+ }
209
+ components.sort((a, b) => {
210
+ const firstA = a[0] ?? "";
211
+ const firstB = b[0] ?? "";
212
+ return firstA.localeCompare(firstB);
213
+ });
214
+ return { components };
215
+ };
216
+ var hasSelfLoop = (nodeId, adjacencyById) => {
217
+ const targets = adjacencyById.get(nodeId) ?? [];
218
+ return targets.includes(nodeId);
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);
227
+ }
228
+ if (component.length > 1) {
229
+ cycles.push({ nodes: [...component] });
230
+ return;
231
+ }
232
+ const onlyNode = component[0];
233
+ if (onlyNode !== void 0 && hasSelfLoop(onlyNode, graph.adjacencyById)) {
234
+ cycles.push({ nodes: [...component] });
235
+ }
236
+ });
237
+ const dagOutgoing = /* @__PURE__ */ new Map();
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;
248
+ }
249
+ const outgoing = dagOutgoing.get(fromComponent);
250
+ if (outgoing?.has(toComponent) === true) {
251
+ continue;
252
+ }
253
+ outgoing?.add(toComponent);
254
+ inDegree.set(toComponent, (inDegree.get(toComponent) ?? 0) + 1);
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);
262
+ }
263
+ }
264
+ let cursor = 0;
265
+ while (cursor < queue.length) {
266
+ const componentId = queue[cursor];
267
+ cursor += 1;
268
+ if (componentId === void 0) {
269
+ continue;
270
+ }
271
+ const currentDepth = depthByComponent.get(componentId) ?? 0;
272
+ const outgoing = dagOutgoing.get(componentId) ?? /* @__PURE__ */ new Set();
273
+ for (const nextComponent of outgoing) {
274
+ const nextDepth = depthByComponent.get(nextComponent) ?? 0;
275
+ if (currentDepth + 1 > nextDepth) {
276
+ depthByComponent.set(nextComponent, currentDepth + 1);
277
+ }
278
+ const remainingIncoming = (inDegree.get(nextComponent) ?? 0) - 1;
279
+ inDegree.set(nextComponent, remainingIncoming);
280
+ if (remainingIncoming === 0) {
281
+ queue.push(nextComponent);
282
+ }
283
+ }
284
+ }
285
+ const depthByNodeId = /* @__PURE__ */ new Map();
286
+ let graphDepth = 0;
287
+ components.forEach((component, componentId) => {
288
+ const componentDepth = depthByComponent.get(componentId) ?? 0;
289
+ if (componentDepth > graphDepth) {
290
+ graphDepth = componentDepth;
291
+ }
292
+ for (const nodeId of component) {
293
+ depthByNodeId.set(nodeId, componentDepth);
294
+ }
295
+ });
296
+ cycles.sort((a, b) => {
297
+ const firstA = a.nodes[0] ?? "";
298
+ const firstB = b.nodes[0] ?? "";
299
+ return firstA.localeCompare(firstB);
300
+ });
301
+ return {
302
+ depthByNodeId,
303
+ graphDepth,
304
+ cycles
305
+ };
306
+ };
307
+ var createGraphAnalysisSummary = (targetPath, graph) => {
308
+ const fanInById = /* @__PURE__ */ new Map();
309
+ const fanOutById = /* @__PURE__ */ new Map();
310
+ for (const node of graph.nodes) {
311
+ fanInById.set(node.id, 0);
312
+ fanOutById.set(node.id, graph.adjacencyById.get(node.id)?.length ?? 0);
313
+ }
314
+ for (const edge of graph.edges) {
315
+ fanInById.set(edge.to, (fanInById.get(edge.to) ?? 0) + 1);
316
+ }
317
+ const { cycles, depthByNodeId, graphDepth } = computeCyclesAndDepth(graph);
318
+ let maxFanIn = 0;
319
+ let maxFanOut = 0;
320
+ const files = graph.nodes.map((node) => {
321
+ const fanIn = fanInById.get(node.id) ?? 0;
322
+ const fanOut = fanOutById.get(node.id) ?? 0;
323
+ if (fanIn > maxFanIn) {
324
+ maxFanIn = fanIn;
325
+ }
326
+ if (fanOut > maxFanOut) {
327
+ maxFanOut = fanOut;
328
+ }
329
+ return {
330
+ id: node.id,
331
+ relativePath: node.relativePath,
332
+ directDependencies: graph.adjacencyById.get(node.id) ?? [],
333
+ fanIn,
334
+ fanOut,
335
+ depth: depthByNodeId.get(node.id) ?? 0
336
+ };
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
+ }
389
+ const normalizedRelativePath = normalizePath(relativePath);
390
+ const segments = normalizedRelativePath.split("/");
391
+ return !segments.some((segment) => IGNORED_SEGMENTS.has(segment));
392
+ };
393
+ var discoverSourceFilesByScan = (projectRoot) => {
394
+ const files = ts.sys.readDirectory(
395
+ projectRoot,
396
+ [...SOURCE_EXTENSIONS],
397
+ SCAN_EXCLUDES,
398
+ SCAN_INCLUDES
399
+ );
400
+ return files.map((filePath) => resolve(filePath));
401
+ };
402
+ var parseTsConfigFile = (configPath) => {
403
+ const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(
404
+ configPath,
405
+ {},
406
+ {
407
+ ...ts.sys,
408
+ onUnRecoverableConfigFileDiagnostic: () => {
409
+ throw new Error(`Failed to parse TypeScript configuration at ${configPath}`);
410
+ }
411
+ }
412
+ );
413
+ if (parsedCommandLine === void 0) {
414
+ throw new Error(`Failed to parse TypeScript configuration at ${configPath}`);
415
+ }
416
+ return parsedCommandLine;
417
+ };
418
+ var collectFilesFromTsConfigGraph = (projectRoot) => {
419
+ const rootConfigPath = ts.findConfigFile(projectRoot, ts.sys.fileExists, "tsconfig.json");
420
+ if (rootConfigPath === void 0) {
421
+ return null;
422
+ }
423
+ const visitedConfigPaths = /* @__PURE__ */ new Set();
424
+ const collectedFiles = /* @__PURE__ */ new Set();
425
+ let rootOptions = null;
426
+ const visitConfig = (configPath) => {
427
+ const absoluteConfigPath = resolve(configPath);
428
+ if (visitedConfigPaths.has(absoluteConfigPath)) {
429
+ return;
430
+ }
431
+ visitedConfigPaths.add(absoluteConfigPath);
432
+ const parsed = parseTsConfigFile(absoluteConfigPath);
433
+ if (rootOptions === null) {
434
+ rootOptions = parsed.options;
435
+ }
436
+ for (const filePath of parsed.fileNames) {
437
+ collectedFiles.add(resolve(filePath));
438
+ }
439
+ for (const reference of parsed.projectReferences ?? []) {
440
+ const referencePath = resolve(reference.path);
441
+ const referenceConfigPath = ts.sys.directoryExists(referencePath) ? ts.findConfigFile(referencePath, ts.sys.fileExists, "tsconfig.json") : referencePath;
442
+ if (referenceConfigPath !== void 0 && ts.sys.fileExists(referenceConfigPath)) {
443
+ visitConfig(referenceConfigPath);
444
+ }
445
+ }
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
+ }
471
+ if (collected.fileNames.length === 0) {
472
+ return {
473
+ fileNames: discoverSourceFilesByScan(projectRoot),
474
+ options: createCompilerOptions(collected.rootOptions),
475
+ tsconfigCount: collected.visitedConfigCount,
476
+ usedFallbackScan: true
477
+ };
478
+ }
479
+ return {
480
+ fileNames: collected.fileNames,
481
+ options: createCompilerOptions(collected.rootOptions),
482
+ tsconfigCount: collected.visitedConfigCount,
483
+ usedFallbackScan: false
484
+ };
485
+ };
486
+ var getSpecifierFromExpression = (expression) => {
487
+ if (ts.isStringLiteral(expression)) {
488
+ return expression.text;
489
+ }
490
+ if (ts.isNoSubstitutionTemplateLiteral(expression)) {
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;
512
+ }
513
+ if (namedBindings.elements.length === 0) {
514
+ return true;
515
+ }
516
+ return namedBindings.elements.some((element) => !element.isTypeOnly);
517
+ };
518
+ var extractModuleSpecifiers = (sourceFile) => {
519
+ const specifiers = /* @__PURE__ */ new Set();
520
+ const visit = (node) => {
521
+ if (ts.isImportDeclaration(node)) {
522
+ if (hasRuntimeImport(node) && node.moduleSpecifier !== void 0) {
523
+ const specifier = getSpecifierFromExpression(node.moduleSpecifier);
524
+ if (specifier !== void 0) {
525
+ specifiers.add(specifier);
526
+ }
527
+ }
528
+ return;
529
+ }
530
+ if (ts.isExportDeclaration(node)) {
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
+ }
537
+ return;
538
+ }
539
+ if (ts.isCallExpression(node)) {
540
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
541
+ const firstArgument = node.arguments[0];
542
+ if (firstArgument !== void 0) {
543
+ const specifier = getSpecifierFromExpression(firstArgument);
544
+ if (specifier !== void 0) {
545
+ specifiers.add(specifier);
546
+ }
547
+ }
548
+ }
549
+ if (ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length > 0) {
550
+ const firstArgument = node.arguments[0];
551
+ if (firstArgument !== void 0) {
552
+ const specifier = getSpecifierFromExpression(firstArgument);
553
+ if (specifier !== void 0) {
554
+ specifiers.add(specifier);
555
+ }
556
+ }
557
+ }
558
+ }
559
+ ts.forEachChild(node, visit);
560
+ };
561
+ visit(sourceFile);
562
+ return [...specifiers];
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) {
592
+ continue;
593
+ }
594
+ const fromNode = nodeByAbsolutePath.get(sourcePath);
595
+ if (fromNode === void 0) {
596
+ continue;
597
+ }
598
+ const moduleSpecifiers = extractModuleSpecifiers(sourceFile);
599
+ for (const specifier of moduleSpecifiers) {
600
+ const cacheKey = `${sourcePath}\0${specifier}`;
601
+ let resolvedPath = resolverCache.get(cacheKey);
602
+ if (resolvedPath === void 0 && !resolverCache.has(cacheKey)) {
603
+ const resolved = ts.resolveModuleName(specifier, sourcePath, options, ts.sys).resolvedModule;
604
+ if (resolved !== void 0) {
605
+ resolvedPath = normalizePath(resolve(resolved.resolvedFileName));
606
+ }
607
+ resolverCache.set(cacheKey, resolvedPath);
608
+ }
609
+ if (resolvedPath === void 0 || !sourceFilePathSet.has(resolvedPath)) {
610
+ continue;
611
+ }
612
+ const toNode = nodeByAbsolutePath.get(resolvedPath);
613
+ if (toNode === void 0) {
614
+ continue;
615
+ }
616
+ edges.push({ from: fromNode.id, to: toNode.id });
617
+ }
618
+ const processed = index + 1;
619
+ if (processed === 1 || processed === uniqueSourceFilePaths.length || processed % 50 === 0) {
620
+ onProgress?.({
621
+ stage: "file_processed",
622
+ processed,
623
+ total: uniqueSourceFilePaths.length,
624
+ filePath: fromNode.id
625
+ });
626
+ }
627
+ }
628
+ onProgress?.({ stage: "edges_resolved", totalEdges: edges.length });
629
+ return {
630
+ nodes: [...nodeByAbsolutePath.values()],
631
+ edges
632
+ };
633
+ };
634
+ var buildProjectGraphSummary = (input) => {
635
+ const parsedProject = parseTypescriptProject(input.projectPath, input.onProgress);
636
+ const graphData = createGraphData(parsedProject.nodes, parsedProject.edges);
637
+ return createGraphAnalysisSummary(input.projectPath, graphData);
638
+ };
639
+
640
+ // ../dependency-firewall/dist/index.js
641
+ import { existsSync, readFileSync } from "fs";
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);
650
+ }
651
+ const normalized = [];
652
+ for (const [name, candidates] of byName.entries()) {
653
+ if (candidates.length === 0) {
654
+ continue;
655
+ }
656
+ candidates.sort((a, b) => b.version.localeCompare(a.version));
657
+ const selected = candidates[0];
658
+ if (selected === void 0) {
659
+ continue;
660
+ }
661
+ const deps = selected.dependencies.map((dep) => {
662
+ const at = dep.lastIndexOf("@");
663
+ return at <= 0 ? dep : dep.slice(0, at);
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
+ });
671
+ }
672
+ return normalized.sort((a, b) => a.name.localeCompare(b.name));
673
+ };
674
+ var computeDepths = (nodeByName, directNames) => {
675
+ const visiting = /* @__PURE__ */ new Set();
676
+ const depthByName = /* @__PURE__ */ new Map();
677
+ const compute = (name) => {
678
+ const known = depthByName.get(name);
679
+ if (known !== void 0) {
680
+ return known;
681
+ }
682
+ if (visiting.has(name)) {
683
+ return 0;
684
+ }
685
+ visiting.add(name);
686
+ const node = nodeByName.get(name);
687
+ if (node === void 0) {
688
+ visiting.delete(name);
689
+ depthByName.set(name, 0);
690
+ return 0;
691
+ }
692
+ let maxChildDepth = 0;
693
+ for (const dependencyName of node.dependencies) {
694
+ const childDepth = compute(dependencyName);
695
+ if (childDepth > maxChildDepth) {
696
+ maxChildDepth = childDepth;
697
+ }
698
+ }
699
+ visiting.delete(name);
700
+ const ownDepth = directNames.has(name) ? 0 : maxChildDepth + 1;
701
+ depthByName.set(name, ownDepth);
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;
711
+ }
712
+ }
713
+ return { depthByName, maxDepth };
714
+ };
715
+ var rankCentrality = (nodes, dependentsByName, directNames, topN) => [...nodes].map((node) => ({
716
+ name: node.name,
717
+ dependents: dependentsByName.get(node.name) ?? 0,
718
+ fanOut: node.dependencies.length,
719
+ direct: directNames.has(node.name)
720
+ })).sort(
721
+ (a, b) => b.dependents - a.dependents || b.fanOut - a.fanOut || a.name.localeCompare(b.name)
722
+ ).slice(0, topN);
723
+ var canPropagateSignal = (signal) => signal === "abandoned" || signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout";
724
+ var collectTransitiveDependencies = (rootName, nodeByName) => {
725
+ const seen = /* @__PURE__ */ new Set();
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;
731
+ }
732
+ seen.add(current);
733
+ const currentNode = nodeByName.get(current);
734
+ if (currentNode === void 0) {
735
+ continue;
736
+ }
737
+ for (const next of currentNode.dependencies) {
738
+ if (!seen.has(next)) {
739
+ stack.push(next);
740
+ }
741
+ }
742
+ }
743
+ return [...seen].sort((a, b) => a.localeCompare(b));
744
+ };
745
+ var buildExternalAnalysisSummary = (targetPath, extraction, metadataByKey, config) => {
746
+ const nodes = normalizeNodes(extraction.nodes);
747
+ const directNames = new Set(extraction.directDependencies.map((dep) => dep.name));
748
+ const directSpecByName = new Map(extraction.directDependencies.map((dep) => [dep.name, dep.requestedRange]));
749
+ const nodeByName = new Map(nodes.map((node) => [node.name, node]));
750
+ const dependentsByName = /* @__PURE__ */ new Map();
751
+ for (const node of nodes) {
752
+ dependentsByName.set(node.name, dependentsByName.get(node.name) ?? 0);
753
+ }
754
+ for (const node of nodes) {
755
+ for (const dependencyName of node.dependencies) {
756
+ if (!nodeByName.has(dependencyName)) {
757
+ continue;
758
+ }
759
+ dependentsByName.set(dependencyName, (dependentsByName.get(dependencyName) ?? 0) + 1);
760
+ }
761
+ }
762
+ const { depthByName, maxDepth } = computeDepths(nodeByName, directNames);
763
+ const centralityRanking = rankCentrality(nodes, dependentsByName, directNames, config.centralityTopN);
764
+ const topCentralNames = new Set(
765
+ centralityRanking.slice(0, Math.max(1, Math.ceil(centralityRanking.length * 0.25))).map((entry) => entry.name)
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;
773
+ }
774
+ const dependencyDepth = depthByName.get(node.name) ?? 0;
775
+ const dependents = dependentsByName.get(node.name) ?? 0;
776
+ const riskSignals = [];
777
+ if ((metadata?.maintainerCount ?? 0) === 1) {
778
+ riskSignals.push("single_maintainer");
779
+ }
780
+ if ((metadata?.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold) {
781
+ riskSignals.push("abandoned");
782
+ }
783
+ if (topCentralNames.has(node.name) && dependents > 0) {
784
+ riskSignals.push("high_centrality");
785
+ }
786
+ if (dependencyDepth >= config.deepChainThreshold) {
787
+ riskSignals.push("deep_chain");
788
+ }
789
+ if (node.dependencies.length >= config.fanOutHighThreshold) {
790
+ riskSignals.push("high_fanout");
791
+ }
792
+ if (metadata === null) {
793
+ riskSignals.push("metadata_unavailable");
794
+ }
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
+ }
814
+ allDependencies.sort((a, b) => a.name.localeCompare(b.name));
815
+ const allByName = new Map(allDependencies.map((dep) => [dep.name, dep]));
816
+ const dependencies = allDependencies.filter((dep) => dep.direct).map((dep) => {
817
+ const transitiveDependencies = collectTransitiveDependencies(dep.name, nodeByName);
818
+ const inheritedSignals = /* @__PURE__ */ new Set();
819
+ const allSignals = new Set(dep.ownRiskSignals);
820
+ for (const transitiveName of transitiveDependencies) {
821
+ const transitive = allByName.get(transitiveName);
822
+ if (transitive === void 0) {
823
+ continue;
824
+ }
825
+ for (const signal of transitive.riskSignals) {
826
+ if (canPropagateSignal(signal)) {
827
+ inheritedSignals.add(signal);
828
+ allSignals.add(signal);
829
+ }
830
+ }
831
+ }
832
+ return {
833
+ ...dep,
834
+ transitiveDependencies,
835
+ inheritedRiskSignals: [...inheritedSignals].sort((a, b) => a.localeCompare(b)),
836
+ riskSignals: [...allSignals].sort((a, b) => a.localeCompare(b))
837
+ };
838
+ }).sort((a, b) => a.name.localeCompare(b.name));
839
+ const highRiskDependencies = dependencies.filter((dep) => dep.riskSignals.length > 1).sort((a, b) => b.riskSignals.length - a.riskSignals.length || a.name.localeCompare(b.name)).slice(0, config.maxHighRiskDependencies).map((dep) => dep.name);
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
867
+ };
868
+ var LOCKFILE_CANDIDATES = [
869
+ { fileName: "pnpm-lock.yaml", kind: "pnpm" },
870
+ { fileName: "package-lock.json", kind: "npm" },
871
+ { fileName: "npm-shrinkwrap.json", kind: "npm-shrinkwrap" },
872
+ { fileName: "yarn.lock", kind: "yarn" },
873
+ { fileName: "bun.lock", kind: "bun" },
874
+ { fileName: "bun.lockb", kind: "bun" }
875
+ ];
876
+ var loadPackageJson = (repositoryPath) => {
877
+ const packageJsonPath2 = join(repositoryPath, "package.json");
878
+ if (!existsSync(packageJsonPath2)) {
879
+ return null;
880
+ }
881
+ return {
882
+ path: packageJsonPath2,
883
+ raw: readFileSync(packageJsonPath2, "utf8")
884
+ };
885
+ };
886
+ var selectLockfile = (repositoryPath) => {
887
+ for (const candidate of LOCKFILE_CANDIDATES) {
888
+ const absolutePath = join(repositoryPath, candidate.fileName);
889
+ if (!existsSync(absolutePath)) {
890
+ continue;
891
+ }
892
+ return {
893
+ path: absolutePath,
894
+ kind: candidate.kind,
895
+ raw: readFileSync(absolutePath, "utf8")
896
+ };
897
+ }
898
+ return null;
899
+ };
900
+ var parsePackageJson = (raw) => {
901
+ const parsed = JSON.parse(raw);
902
+ const merged = /* @__PURE__ */ new Map();
903
+ for (const block of [
904
+ parsed.dependencies,
905
+ parsed.devDependencies,
906
+ parsed.optionalDependencies,
907
+ parsed.peerDependencies
908
+ ]) {
909
+ if (block === void 0) {
910
+ continue;
911
+ }
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
+ }
972
+ return { name, version: version2 };
973
+ };
974
+ var parsePnpmLockfile = (raw, directSpecs) => {
975
+ const lines = raw.split("\n");
976
+ let state = "root";
977
+ let currentPackage = null;
978
+ let currentDependencyName = null;
979
+ const dependenciesByNode = /* @__PURE__ */ new Map();
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
+ }
1035
+ }
1036
+ }
1037
+ const nodes = [...dependenciesByNode.entries()].map(([nodeId, deps]) => {
1038
+ const at = nodeId.lastIndexOf("@");
1039
+ return {
1040
+ name: nodeId.slice(0, at),
1041
+ version: nodeId.slice(at + 1),
1042
+ dependencies: [...deps].sort((a, b) => a.localeCompare(b))
1043
+ };
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
+ }
1059
+ const lastAt = selector.lastIndexOf("@");
1060
+ if (lastAt <= 0) {
1061
+ return null;
1062
+ }
1063
+ return selector.slice(lastAt + 1);
1064
+ };
1065
+ var parseYarnLock = (raw, directSpecs) => {
1066
+ const lines = raw.split("\n");
1067
+ const nodes = [];
1068
+ let selectors = [];
1069
+ let version2 = null;
1070
+ let readingDependencies = false;
1071
+ let dependencies = [];
1072
+ const flushEntry = () => {
1073
+ if (selectors.length === 0 || version2 === null) {
1074
+ selectors = [];
1075
+ version2 = null;
1076
+ dependencies = [];
1077
+ readingDependencies = false;
1078
+ return;
1079
+ }
1080
+ for (const selector of selectors) {
1081
+ const parsedVersion = parseVersionSelector(selector);
1082
+ const at = selector.lastIndexOf("@");
1083
+ const name = at <= 0 ? selector : selector.slice(0, at);
1084
+ if (name.length === 0) {
1085
+ continue;
1086
+ }
1087
+ nodes.push({
1088
+ name,
1089
+ version: version2,
1090
+ dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
1091
+ });
1092
+ if (parsedVersion !== null) {
1093
+ nodes.push({
1094
+ name,
1095
+ version: parsedVersion,
1096
+ dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
1097
+ });
1098
+ }
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
+ continue;
1108
+ }
1109
+ if (!line.startsWith(" ") && line.endsWith(":")) {
1110
+ flushEntry();
1111
+ const keyText = line.slice(0, -1);
1112
+ selectors = keyText.split(",").map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
1113
+ continue;
1114
+ }
1115
+ if (line.match(/^\s{2}version\s+/) !== null) {
1116
+ const value = line.replace(/^\s{2}version\s+/, "").trim();
1117
+ version2 = stripQuotes(value);
1118
+ readingDependencies = false;
1119
+ continue;
1120
+ }
1121
+ if (line.match(/^\s{2}dependencies:\s*$/) !== null) {
1122
+ readingDependencies = true;
1123
+ continue;
1124
+ }
1125
+ if (readingDependencies && line.match(/^\s{4}[^\s].+$/) !== null) {
1126
+ const depLine = line.trim();
1127
+ const firstSpace = depLine.indexOf(" ");
1128
+ if (firstSpace <= 0) {
1129
+ continue;
1130
+ }
1131
+ const depName = stripQuotes(depLine.slice(0, firstSpace));
1132
+ const depRef = stripQuotes(depLine.slice(firstSpace + 1).trim());
1133
+ const depVersion = parseVersionSelector(depRef) ?? depRef;
1134
+ dependencies.push(`${depName}@${depVersion}`);
1135
+ continue;
1136
+ }
1137
+ readingDependencies = false;
1138
+ }
1139
+ flushEntry();
1140
+ const deduped = /* @__PURE__ */ new Map();
1141
+ for (const node of nodes) {
1142
+ const key = `${node.name}@${node.version}`;
1143
+ if (!deduped.has(key)) {
1144
+ deduped.set(key, node);
1145
+ }
1146
+ }
1147
+ return {
1148
+ kind: "yarn",
1149
+ directDependencies: directSpecs,
1150
+ nodes: [...deduped.values()].sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version))
1151
+ };
1152
+ };
1153
+ var parseBunLock = (_raw, _directSpecs) => {
1154
+ throw new Error("unsupported_lockfile_format");
1155
+ };
1156
+ var withDefaults = (overrides) => ({
1157
+ ...DEFAULT_EXTERNAL_ANALYSIS_CONFIG,
1158
+ ...overrides
1159
+ });
1160
+ var parseExtraction = (lockfileKind, lockfileRaw, directSpecs) => {
1161
+ switch (lockfileKind) {
1162
+ case "pnpm":
1163
+ return parsePnpmLockfile(lockfileRaw, directSpecs);
1164
+ case "npm":
1165
+ case "npm-shrinkwrap":
1166
+ return {
1167
+ ...parsePackageLock(lockfileRaw, directSpecs),
1168
+ kind: lockfileKind
1169
+ };
1170
+ case "yarn":
1171
+ return parseYarnLock(lockfileRaw, directSpecs);
1172
+ case "bun":
1173
+ return parseBunLock(lockfileRaw, directSpecs);
1174
+ default:
1175
+ throw new Error("unsupported_lockfile_format");
1176
+ }
1177
+ };
1178
+ var mapWithConcurrency = async (values, limit, handler) => {
1179
+ const effectiveLimit = Math.max(1, limit);
1180
+ const results = new Array(values.length);
1181
+ let index = 0;
1182
+ const workers = Array.from({ length: Math.min(effectiveLimit, values.length) }, async () => {
1183
+ for (; ; ) {
1184
+ const current = index;
1185
+ index += 1;
1186
+ if (current >= values.length) {
1187
+ return;
1188
+ }
1189
+ const value = values[current];
1190
+ if (value === void 0) {
1191
+ return;
1192
+ }
1193
+ results[current] = await handler(value);
1194
+ }
1195
+ });
1196
+ await Promise.all(workers);
1197
+ return results;
1198
+ };
1199
+ var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
1200
+ const packageJson = loadPackageJson(input.repositoryPath);
1201
+ if (packageJson === null) {
1202
+ return {
1203
+ targetPath: input.repositoryPath,
1204
+ available: false,
1205
+ reason: "package_json_not_found"
1206
+ };
1207
+ }
1208
+ 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
+ try {
1219
+ const directSpecs = parsePackageJson(packageJson.raw);
1220
+ const extraction = parseExtraction(lockfile.kind, lockfile.raw, directSpecs);
1221
+ const config = withDefaults(input.config);
1222
+ onProgress?.({
1223
+ stage: "lockfile_parsed",
1224
+ dependencyNodes: extraction.nodes.length,
1225
+ directDependencies: extraction.directDependencies.length
1226
+ });
1227
+ onProgress?.({ stage: "metadata_fetch_started", total: extraction.nodes.length });
1228
+ let completed = 0;
1229
+ const metadataEntries = await mapWithConcurrency(
1230
+ extraction.nodes,
1231
+ config.metadataRequestConcurrency,
1232
+ async (node) => {
1233
+ const result = {
1234
+ key: `${node.name}@${node.version}`,
1235
+ metadata: await metadataProvider.getMetadata(node.name, node.version)
1236
+ };
1237
+ completed += 1;
1238
+ onProgress?.({
1239
+ stage: "metadata_fetch_progress",
1240
+ completed,
1241
+ total: extraction.nodes.length,
1242
+ packageName: node.name
1243
+ });
1244
+ return result;
1245
+ }
1246
+ );
1247
+ onProgress?.({ stage: "metadata_fetch_completed", total: extraction.nodes.length });
1248
+ const metadataByKey = /* @__PURE__ */ new Map();
1249
+ for (const entry of metadataEntries) {
1250
+ metadataByKey.set(entry.key, entry.metadata);
1251
+ }
1252
+ const summary = buildExternalAnalysisSummary(input.repositoryPath, extraction, metadataByKey, config);
1253
+ if (summary.available) {
1254
+ onProgress?.({
1255
+ stage: "summary_built",
1256
+ totalDependencies: summary.metrics.totalDependencies,
1257
+ directDependencies: summary.metrics.directDependencies
1258
+ });
1259
+ }
1260
+ return summary;
1261
+ } catch (error) {
1262
+ const message = error instanceof Error ? error.message : "unknown";
1263
+ if (message.includes("unsupported_lockfile_format")) {
1264
+ return {
1265
+ targetPath: input.repositoryPath,
1266
+ available: false,
1267
+ reason: "unsupported_lockfile_format"
1268
+ };
1269
+ }
1270
+ return {
1271
+ targetPath: input.repositoryPath,
1272
+ available: false,
1273
+ reason: "invalid_lockfile"
1274
+ };
1275
+ }
1276
+ };
1277
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
1278
+ var round42 = (value) => Number(value.toFixed(4));
1279
+ var parseDate = (iso) => {
1280
+ if (iso === void 0) {
1281
+ return null;
1282
+ }
1283
+ const value = Date.parse(iso);
1284
+ return Number.isNaN(value) ? null : value;
1285
+ };
1286
+ var NpmRegistryMetadataProvider = class {
1287
+ cache = /* @__PURE__ */ new Map();
1288
+ async getMetadata(name, version2) {
1289
+ const key = `${name}@${version2}`;
1290
+ if (this.cache.has(key)) {
1291
+ return this.cache.get(key) ?? null;
1292
+ }
1293
+ try {
1294
+ const encodedName = encodeURIComponent(name);
1295
+ const response = await fetch(`https://registry.npmjs.org/${encodedName}`);
1296
+ if (!response.ok) {
1297
+ this.cache.set(key, null);
1298
+ return null;
1299
+ }
1300
+ const payload = await response.json();
1301
+ const timeEntries = payload.time ?? {};
1302
+ const publishDates = Object.entries(timeEntries).filter(([tag]) => tag !== "created" && tag !== "modified").map(([, date]) => parseDate(date)).filter((value) => value !== null).sort((a, b) => a - b);
1303
+ const modifiedAt = parseDate(timeEntries["modified"]);
1304
+ const now = Date.now();
1305
+ const daysSinceLastRelease = modifiedAt === null ? null : Math.max(0, round42((now - modifiedAt) / ONE_DAY_MS));
1306
+ let releaseFrequencyDays = null;
1307
+ if (publishDates.length >= 2) {
1308
+ const totalIntervals = publishDates.length - 1;
1309
+ let sum = 0;
1310
+ for (let i = 1; i < publishDates.length; i += 1) {
1311
+ const current = publishDates[i];
1312
+ const previous = publishDates[i - 1];
1313
+ if (current !== void 0 && previous !== void 0) {
1314
+ sum += current - previous;
1315
+ }
1316
+ }
1317
+ releaseFrequencyDays = round42(sum / totalIntervals / ONE_DAY_MS);
1318
+ }
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
+ }
1336
+ }
1337
+ };
1338
+ var NoopMetadataProvider = class {
1339
+ async getMetadata(_name, _version) {
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
+
1348
+ // ../git-analyzer/dist/index.js
1349
+ import { execFileSync } from "child_process";
1350
+ var pairKey = (a, b) => `${a}\0${b}`;
1351
+ var round43 = (value) => Number(value.toFixed(4));
1352
+ var normalizeName = (value) => value.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
1353
+ var extractEmailStem = (authorId) => {
1354
+ const normalized = authorId.trim().toLowerCase();
1355
+ const githubNoReplyMatch = normalized.match(/^\d+\+([^@]+)@users\.noreply\.github\.com$/);
1356
+ if (githubNoReplyMatch?.[1] !== void 0) {
1357
+ return githubNoReplyMatch[1].replace(/[._+-]/g, "");
1358
+ }
1359
+ const atIndex = normalized.indexOf("@");
1360
+ if (atIndex <= 0) {
1361
+ return null;
1362
+ }
1363
+ return normalized.slice(0, atIndex).replace(/[._+-]/g, "");
1364
+ };
1365
+ var areNamesCompatible = (left, right) => {
1366
+ if (left.length === 0 || right.length === 0) {
1367
+ return false;
1368
+ }
1369
+ if (left === right) {
1370
+ return true;
1371
+ }
1372
+ if (left.startsWith(`${right} `) || right.startsWith(`${left} `)) {
1373
+ return true;
1374
+ }
1375
+ return false;
1376
+ };
1377
+ var chooseCanonicalAuthorId = (profiles) => {
1378
+ const ordered = [...profiles].sort((a, b) => {
1379
+ const aIsNoReply = a.authorId.includes("@users.noreply.github.com");
1380
+ const bIsNoReply = b.authorId.includes("@users.noreply.github.com");
1381
+ if (aIsNoReply !== bIsNoReply) {
1382
+ return aIsNoReply ? 1 : -1;
1383
+ }
1384
+ if (a.commitCount !== b.commitCount) {
1385
+ return b.commitCount - a.commitCount;
1386
+ }
1387
+ return a.authorId.localeCompare(b.authorId);
1388
+ });
1389
+ return ordered[0]?.authorId ?? "";
1390
+ };
1391
+ var buildAuthorAliasMap = (commits) => {
1392
+ const nameCountsByAuthorId = /* @__PURE__ */ new Map();
1393
+ const commitCountByAuthorId = /* @__PURE__ */ new Map();
1394
+ for (const commit of commits) {
1395
+ commitCountByAuthorId.set(commit.authorId, (commitCountByAuthorId.get(commit.authorId) ?? 0) + 1);
1396
+ const normalizedName = normalizeName(commit.authorName);
1397
+ const names = nameCountsByAuthorId.get(commit.authorId) ?? /* @__PURE__ */ new Map();
1398
+ if (normalizedName.length > 0) {
1399
+ names.set(normalizedName, (names.get(normalizedName) ?? 0) + 1);
1400
+ }
1401
+ nameCountsByAuthorId.set(commit.authorId, names);
1402
+ }
1403
+ const profiles = [...commitCountByAuthorId.entries()].map(([authorId, commitCount]) => {
1404
+ const names = nameCountsByAuthorId.get(authorId);
1405
+ const primaryName = names === void 0 ? "" : [...names.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]?.[0] ?? "";
1406
+ const normalizedAuthorId = authorId.toLowerCase();
1407
+ const isBot = normalizedAuthorId.includes("[bot]");
1408
+ return {
1409
+ authorId,
1410
+ commitCount,
1411
+ primaryName,
1412
+ emailStem: isBot ? null : extractEmailStem(authorId),
1413
+ isBot
1414
+ };
1415
+ });
1416
+ const groupsByStem = /* @__PURE__ */ new Map();
1417
+ for (const profile of profiles) {
1418
+ if (profile.emailStem === null || profile.emailStem.length < 4) {
1419
+ continue;
1420
+ }
1421
+ const current = groupsByStem.get(profile.emailStem) ?? [];
1422
+ current.push(profile);
1423
+ groupsByStem.set(profile.emailStem, current);
1424
+ }
1425
+ const aliasMap = /* @__PURE__ */ new Map();
1426
+ for (const profile of profiles) {
1427
+ aliasMap.set(profile.authorId, profile.authorId);
1428
+ }
1429
+ for (const group of groupsByStem.values()) {
1430
+ if (group.length < 2) {
1431
+ continue;
1432
+ }
1433
+ const compatible = [];
1434
+ for (const profile of group) {
1435
+ if (profile.isBot || profile.primaryName.length === 0) {
1436
+ continue;
1437
+ }
1438
+ compatible.push(profile);
1439
+ }
1440
+ if (compatible.length < 2) {
1441
+ continue;
1442
+ }
1443
+ const canonical = chooseCanonicalAuthorId(compatible);
1444
+ const canonicalProfile = compatible.find((candidate) => candidate.authorId === canonical);
1445
+ if (canonicalProfile === void 0) {
1446
+ continue;
1447
+ }
1448
+ for (const profile of compatible) {
1449
+ if (areNamesCompatible(profile.primaryName, canonicalProfile.primaryName)) {
1450
+ aliasMap.set(profile.authorId, canonical);
1451
+ }
1452
+ }
1453
+ }
1454
+ return aliasMap;
1455
+ };
1456
+ var computeBusFactor = (authorDistribution, threshold) => {
1457
+ if (authorDistribution.length === 0) {
1458
+ return 0;
1459
+ }
1460
+ let coveredShare = 0;
1461
+ for (let i = 0; i < authorDistribution.length; i += 1) {
1462
+ const entry = authorDistribution[i];
1463
+ if (entry === void 0) {
1464
+ continue;
1465
+ }
1466
+ coveredShare += entry.share;
1467
+ if (coveredShare >= threshold) {
1468
+ return i + 1;
1469
+ }
1470
+ }
1471
+ return authorDistribution.length;
1472
+ };
1473
+ var finalizeAuthorDistribution = (authorCommits) => {
1474
+ const totalCommits = [...authorCommits.values()].reduce((sum, value) => sum + value, 0);
1475
+ if (totalCommits === 0) {
1476
+ return [];
1477
+ }
1478
+ return [...authorCommits.entries()].map(([authorId, commits]) => ({
1479
+ authorId,
1480
+ commits,
1481
+ share: round43(commits / totalCommits)
1482
+ })).sort((a, b) => b.commits - a.commits || a.authorId.localeCompare(b.authorId));
1483
+ };
1484
+ var buildCouplingMatrix = (coChangeByPair, fileCommitCount, consideredCommits, skippedLargeCommits, maxCouplingPairs) => {
1485
+ const allPairs = [];
1486
+ for (const [key, coChangeCommits] of coChangeByPair.entries()) {
1487
+ const [fileA, fileB] = key.split("\0");
1488
+ if (fileA === void 0 || fileB === void 0) {
1489
+ continue;
1490
+ }
1491
+ const fileACommits = fileCommitCount.get(fileA) ?? 0;
1492
+ const fileBCommits = fileCommitCount.get(fileB) ?? 0;
1493
+ const denominator = fileACommits + fileBCommits - coChangeCommits;
1494
+ const couplingScore = denominator === 0 ? 0 : round43(coChangeCommits / denominator);
1495
+ allPairs.push({
1496
+ fileA,
1497
+ fileB,
1498
+ coChangeCommits,
1499
+ couplingScore
1500
+ });
1501
+ }
1502
+ allPairs.sort(
1503
+ (a, b) => b.coChangeCommits - a.coChangeCommits || b.couplingScore - a.couplingScore || a.fileA.localeCompare(b.fileA) || a.fileB.localeCompare(b.fileB)
1504
+ );
1505
+ const truncated = allPairs.length > maxCouplingPairs;
1506
+ return {
1507
+ pairs: truncated ? allPairs.slice(0, maxCouplingPairs) : allPairs,
1508
+ totalPairCount: allPairs.length,
1509
+ consideredCommits,
1510
+ skippedLargeCommits,
1511
+ truncated
1512
+ };
1513
+ };
1514
+ var selectHotspots = (files, config) => {
1515
+ if (files.length === 0) {
1516
+ return { hotspots: [], threshold: 0 };
1517
+ }
1518
+ const sorted = [...files].sort(
1519
+ (a, b) => b.commitCount - a.commitCount || b.churnTotal - a.churnTotal || a.filePath.localeCompare(b.filePath)
1520
+ );
1521
+ const hotspotCount = Math.max(config.hotspotMinFiles, Math.ceil(sorted.length * config.hotspotTopPercent));
1522
+ const selected = sorted.slice(0, hotspotCount);
1523
+ const hotspots = selected.map((file, index) => ({
1524
+ filePath: file.filePath,
1525
+ rank: index + 1,
1526
+ commitCount: file.commitCount,
1527
+ churnTotal: file.churnTotal
1528
+ }));
1529
+ const threshold = selected[selected.length - 1]?.commitCount ?? 0;
1530
+ return { hotspots, threshold };
1531
+ };
1532
+ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
1533
+ const authorAliasById = config.authorIdentityMode === "likely_merge" ? buildAuthorAliasMap(commits) : /* @__PURE__ */ new Map();
1534
+ const fileStats = /* @__PURE__ */ new Map();
1535
+ const coChangeByPair = /* @__PURE__ */ new Map();
1536
+ const headCommitTimestamp = commits.length === 0 ? null : commits[commits.length - 1]?.authoredAtUnix ?? null;
1537
+ const recentWindowStart = headCommitTimestamp === null ? Number.NEGATIVE_INFINITY : headCommitTimestamp - config.recentWindowDays * 24 * 60 * 60;
1538
+ let consideredCommits = 0;
1539
+ let skippedLargeCommits = 0;
1540
+ for (const commit of commits) {
1541
+ const uniqueFiles = /* @__PURE__ */ new Set();
1542
+ for (const fileChange of commit.fileChanges) {
1543
+ uniqueFiles.add(fileChange.filePath);
1544
+ const current = fileStats.get(fileChange.filePath) ?? {
1545
+ commitCount: 0,
1546
+ recentCommitCount: 0,
1547
+ churnAdded: 0,
1548
+ churnDeleted: 0,
1549
+ authors: /* @__PURE__ */ new Map()
1550
+ };
1551
+ current.churnAdded += fileChange.additions;
1552
+ current.churnDeleted += fileChange.deletions;
1553
+ fileStats.set(fileChange.filePath, current);
1554
+ }
1555
+ for (const filePath of uniqueFiles) {
1556
+ const current = fileStats.get(filePath);
1557
+ if (current === void 0) {
1558
+ continue;
1559
+ }
1560
+ current.commitCount += 1;
1561
+ if (commit.authoredAtUnix >= recentWindowStart) {
1562
+ current.recentCommitCount += 1;
1563
+ }
1564
+ const effectiveAuthorId = authorAliasById.get(commit.authorId) ?? commit.authorId;
1565
+ current.authors.set(effectiveAuthorId, (current.authors.get(effectiveAuthorId) ?? 0) + 1);
1566
+ }
1567
+ const orderedFiles = [...uniqueFiles].sort((a, b) => a.localeCompare(b));
1568
+ if (orderedFiles.length > 1) {
1569
+ if (orderedFiles.length <= config.maxFilesPerCommitForCoupling) {
1570
+ consideredCommits += 1;
1571
+ for (let i = 0; i < orderedFiles.length - 1; i += 1) {
1572
+ for (let j = i + 1; j < orderedFiles.length; j += 1) {
1573
+ const fileA = orderedFiles[i];
1574
+ const fileB = orderedFiles[j];
1575
+ if (fileA === void 0 || fileB === void 0) {
1576
+ continue;
1577
+ }
1578
+ const key = pairKey(fileA, fileB);
1579
+ coChangeByPair.set(key, (coChangeByPair.get(key) ?? 0) + 1);
1580
+ }
1581
+ }
1582
+ } else {
1583
+ skippedLargeCommits += 1;
1584
+ }
1585
+ }
1586
+ }
1587
+ const files = [...fileStats.entries()].map(([filePath, stats]) => {
1588
+ const authorDistribution = finalizeAuthorDistribution(stats.authors);
1589
+ const topAuthorShare = authorDistribution[0]?.share ?? 0;
1590
+ return {
1591
+ filePath,
1592
+ commitCount: stats.commitCount,
1593
+ frequencyPer100Commits: commits.length === 0 ? 0 : round43(stats.commitCount / commits.length * 100),
1594
+ churnAdded: stats.churnAdded,
1595
+ churnDeleted: stats.churnDeleted,
1596
+ churnTotal: stats.churnAdded + stats.churnDeleted,
1597
+ recentCommitCount: stats.recentCommitCount,
1598
+ recentVolatility: stats.commitCount === 0 ? 0 : round43(stats.recentCommitCount / stats.commitCount),
1599
+ topAuthorShare,
1600
+ busFactor: computeBusFactor(authorDistribution, config.busFactorCoverageThreshold),
1601
+ authorDistribution
1602
+ };
1603
+ }).sort((a, b) => a.filePath.localeCompare(b.filePath));
1604
+ const fileCommitCount = new Map(files.map((file) => [file.filePath, file.commitCount]));
1605
+ const coupling = buildCouplingMatrix(
1606
+ coChangeByPair,
1607
+ fileCommitCount,
1608
+ consideredCommits,
1609
+ skippedLargeCommits,
1610
+ config.maxCouplingPairs
1611
+ );
1612
+ const { hotspots, threshold } = selectHotspots(files, config);
1613
+ return {
1614
+ targetPath,
1615
+ available: true,
1616
+ files,
1617
+ hotspots,
1618
+ coupling,
1619
+ metrics: {
1620
+ totalCommits: commits.length,
1621
+ totalFiles: files.length,
1622
+ headCommitTimestamp,
1623
+ recentWindowDays: config.recentWindowDays,
1624
+ hotspotTopPercent: config.hotspotTopPercent,
1625
+ hotspotThresholdCommitCount: threshold
1626
+ }
1627
+ };
1628
+ };
1629
+ var DEFAULT_EVOLUTION_CONFIG = {
1630
+ authorIdentityMode: "likely_merge",
1631
+ recentWindowDays: 30,
1632
+ hotspotTopPercent: 0.1,
1633
+ hotspotMinFiles: 1,
1634
+ maxFilesPerCommitForCoupling: 200,
1635
+ maxCouplingPairs: 500,
1636
+ busFactorCoverageThreshold: 0.6
1637
+ };
1638
+ var createEffectiveConfig = (overrides) => ({
1639
+ ...DEFAULT_EVOLUTION_CONFIG,
1640
+ ...overrides
1641
+ });
1642
+ var analyzeRepositoryEvolution = (input, historyProvider, onProgress) => {
1643
+ onProgress?.({ stage: "checking_git_repository" });
1644
+ if (!historyProvider.isGitRepository(input.repositoryPath)) {
1645
+ onProgress?.({ stage: "not_git_repository" });
1646
+ return {
1647
+ targetPath: input.repositoryPath,
1648
+ available: false,
1649
+ reason: "not_git_repository"
1650
+ };
1651
+ }
1652
+ onProgress?.({ stage: "loading_commit_history" });
1653
+ const commits = historyProvider.getCommitHistory(
1654
+ input.repositoryPath,
1655
+ (event) => onProgress?.({ stage: "history", event })
1656
+ );
1657
+ const config = createEffectiveConfig(input.config);
1658
+ onProgress?.({ stage: "computing_metrics" });
1659
+ const summary = computeRepositoryEvolutionSummary(input.repositoryPath, commits, config);
1660
+ onProgress?.({ stage: "analysis_completed", available: summary.available });
1661
+ return summary;
1662
+ };
1663
+ var GitCommandError = class extends Error {
1664
+ args;
1665
+ constructor(message, args) {
1666
+ super(message);
1667
+ this.name = "GitCommandError";
1668
+ this.args = args;
1669
+ }
1670
+ };
1671
+ var ExecGitCommandClient = class {
1672
+ run(repositoryPath, args) {
1673
+ try {
1674
+ return execFileSync("git", ["-C", repositoryPath, ...args], {
1675
+ encoding: "utf8",
1676
+ maxBuffer: 1024 * 1024 * 64,
1677
+ stdio: ["ignore", "pipe", "pipe"]
1678
+ });
1679
+ } catch (error) {
1680
+ const message = error instanceof Error ? error.message : "Unknown git execution error";
1681
+ throw new GitCommandError(message, args);
1682
+ }
1683
+ }
1684
+ };
1685
+ var COMMIT_RECORD_SEPARATOR = "";
1686
+ var COMMIT_FIELD_SEPARATOR = "";
1687
+ var GIT_LOG_FORMAT = `%x1e%H%x1f%at%x1f%an%x1f%ae`;
1688
+ var mapParseProgressToHistoryProgress = (event) => ({
1689
+ stage: "git_log_parse_progress",
1690
+ parsedRecords: event.parsedRecords,
1691
+ totalRecords: event.totalRecords
1692
+ });
1693
+ var parseInteger = (value) => {
1694
+ if (value.length === 0) {
1695
+ return null;
1696
+ }
1697
+ const parsed = Number.parseInt(value, 10);
1698
+ if (Number.isNaN(parsed)) {
1699
+ return null;
1700
+ }
1701
+ return parsed;
1702
+ };
1703
+ var normalizeAuthorIdentity = (authorName, authorEmail) => {
1704
+ const normalizedName = authorName.trim().replace(/\s+/g, " ").toLowerCase();
1705
+ const normalizedEmail = authorEmail.trim().toLowerCase();
1706
+ if (/\[bot\]/i.test(normalizedName) || /\[bot\]/i.test(normalizedEmail)) {
1707
+ return normalizedEmail.length > 0 ? normalizedEmail : normalizedName;
1708
+ }
1709
+ const githubNoReplyMatch = normalizedEmail.match(/^\d+\+([^@]+)@users\.noreply\.github\.com$/);
1710
+ const githubHandle = githubNoReplyMatch?.[1]?.trim().toLowerCase();
1711
+ if (githubHandle !== void 0 && githubHandle.length > 0) {
1712
+ return `${githubHandle}@users.noreply.github.com`;
1713
+ }
1714
+ if (normalizedEmail.length > 0) {
1715
+ return normalizedEmail;
1716
+ }
1717
+ return normalizedName;
1718
+ };
1719
+ var parseRenamedPath = (pathSpec) => {
1720
+ if (!pathSpec.includes(" => ")) {
1721
+ return pathSpec;
1722
+ }
1723
+ const braceRenameMatch = pathSpec.match(/^(.*)\{(.+) => (.+)\}(.*)$/);
1724
+ if (braceRenameMatch !== null) {
1725
+ const [, prefix, , renamedTo, suffix] = braceRenameMatch;
1726
+ return `${prefix}${renamedTo}${suffix}`;
1727
+ }
1728
+ const parts = pathSpec.split(" => ");
1729
+ const finalPart = parts[parts.length - 1];
1730
+ return finalPart ?? pathSpec;
1731
+ };
1732
+ var parseNumstatLine = (line) => {
1733
+ const parts = line.split(" ");
1734
+ if (parts.length < 3) {
1735
+ return null;
1736
+ }
1737
+ const additionsRaw = parts[0];
1738
+ const deletionsRaw = parts[1];
1739
+ const pathRaw = parts.slice(2).join(" ");
1740
+ if (additionsRaw === void 0 || deletionsRaw === void 0) {
1741
+ return null;
1742
+ }
1743
+ const additions = additionsRaw === "-" ? 0 : parseInteger(additionsRaw);
1744
+ const deletions = deletionsRaw === "-" ? 0 : parseInteger(deletionsRaw);
1745
+ if (additions === null || deletions === null) {
1746
+ return null;
1747
+ }
1748
+ const filePath = parseRenamedPath(pathRaw);
1749
+ return {
1750
+ filePath,
1751
+ additions,
1752
+ deletions
1753
+ };
1754
+ };
1755
+ var parseGitLog = (rawLog, onProgress) => {
1756
+ const records = rawLog.split(COMMIT_RECORD_SEPARATOR).map((record) => record.trim()).filter((record) => record.length > 0);
1757
+ const commits = [];
1758
+ for (const [index, record] of records.entries()) {
1759
+ const lines = record.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0);
1760
+ if (lines.length === 0) {
1761
+ continue;
1762
+ }
1763
+ const headerParts = lines[0]?.split(COMMIT_FIELD_SEPARATOR) ?? [];
1764
+ if (headerParts.length !== 4) {
1765
+ continue;
1766
+ }
1767
+ const [hash, authoredAtRaw, authorName, authorEmail] = headerParts;
1768
+ if (hash === void 0 || authoredAtRaw === void 0 || authorName === void 0 || authorEmail === void 0) {
1769
+ continue;
1770
+ }
1771
+ const authoredAtUnix = parseInteger(authoredAtRaw);
1772
+ if (authoredAtUnix === null) {
1773
+ continue;
1774
+ }
1775
+ const fileChanges = [];
1776
+ for (const line of lines.slice(1)) {
1777
+ const parsedLine = parseNumstatLine(line);
1778
+ if (parsedLine !== null) {
1779
+ fileChanges.push(parsedLine);
1780
+ }
1781
+ }
1782
+ commits.push({
1783
+ hash,
1784
+ authorId: normalizeAuthorIdentity(authorName, authorEmail),
1785
+ authorName,
1786
+ authoredAtUnix,
1787
+ fileChanges
1788
+ });
1789
+ const parsedRecords = index + 1;
1790
+ if (parsedRecords === 1 || parsedRecords === records.length || parsedRecords % 500 === 0) {
1791
+ onProgress?.({ parsedRecords, totalRecords: records.length });
1792
+ }
1793
+ }
1794
+ commits.sort((a, b) => a.authoredAtUnix - b.authoredAtUnix || a.hash.localeCompare(b.hash));
1795
+ return commits;
1796
+ };
1797
+ var NON_GIT_CODES = ["not a git repository", "not in a git directory"];
1798
+ var isNotGitError = (error) => {
1799
+ const lower = error.message.toLowerCase();
1800
+ return NON_GIT_CODES.some((code) => lower.includes(code));
1801
+ };
1802
+ var GitCliHistoryProvider = class {
1803
+ constructor(gitClient) {
1804
+ this.gitClient = gitClient;
1805
+ }
1806
+ isGitRepository(repositoryPath) {
1807
+ try {
1808
+ const output = this.gitClient.run(repositoryPath, ["rev-parse", "--is-inside-work-tree"]);
1809
+ return output.trim() === "true";
1810
+ } catch (error) {
1811
+ if (error instanceof GitCommandError && isNotGitError(error)) {
1812
+ return false;
1813
+ }
1814
+ throw error;
1815
+ }
1816
+ }
1817
+ getCommitHistory(repositoryPath, onProgress) {
1818
+ const output = this.gitClient.run(repositoryPath, [
1819
+ "-c",
1820
+ "core.quotepath=false",
1821
+ "log",
1822
+ "--use-mailmap",
1823
+ "--no-merges",
1824
+ "--date=unix",
1825
+ `--pretty=format:${GIT_LOG_FORMAT}`,
1826
+ "--numstat",
1827
+ "--find-renames"
1828
+ ]);
1829
+ onProgress?.({ stage: "git_log_received", bytes: Buffer.byteLength(output, "utf8") });
1830
+ const commits = parseGitLog(output, (event) => onProgress?.(mapParseProgressToHistoryProgress(event)));
1831
+ onProgress?.({ stage: "git_log_parsed", commits: commits.length });
1832
+ return commits;
1833
+ }
1834
+ };
1835
+ var analyzeRepositoryEvolutionFromGit = (input, onProgress) => {
1836
+ const historyProvider = new GitCliHistoryProvider(new ExecGitCommandClient());
1837
+ return analyzeRepositoryEvolution(input, historyProvider, onProgress);
1838
+ };
1839
+
1840
+ // ../risk-engine/dist/index.js
1841
+ var DEFAULT_RISK_ENGINE_CONFIG = {
1842
+ // Base dimensional influence. Risk is never dominated by a single dimension by default.
1843
+ dimensionWeights: {
1844
+ structural: 0.44,
1845
+ evolution: 0.36,
1846
+ external: 0.2
1847
+ },
1848
+ // Interaction terms activate only when both related dimensions are high.
1849
+ interactionWeights: {
1850
+ structuralEvolution: 0.35,
1851
+ centralInstability: 0.25,
1852
+ dependencyAmplification: 0.2
1853
+ },
1854
+ structuralFactorWeights: {
1855
+ fanIn: 0.3,
1856
+ fanOut: 0.25,
1857
+ depth: 0.2,
1858
+ cycleParticipation: 0.25
1859
+ },
1860
+ evolutionFactorWeights: {
1861
+ frequency: 0.26,
1862
+ churn: 0.24,
1863
+ recentVolatility: 0.2,
1864
+ ownershipConcentration: 0.18,
1865
+ busFactorRisk: 0.12
1866
+ },
1867
+ dependencyFactorWeights: {
1868
+ signals: 0.38,
1869
+ staleness: 0.16,
1870
+ maintainerConcentration: 0.16,
1871
+ transitiveBurden: 0.1,
1872
+ centrality: 0.08,
1873
+ chainDepth: 0.06,
1874
+ busFactorRisk: 0.06
1875
+ },
1876
+ quantileClamp: {
1877
+ lower: 0.05,
1878
+ upper: 0.95
1879
+ },
1880
+ hotspotTopPercent: 0.12,
1881
+ hotspotMinFiles: 3,
1882
+ hotspotMaxFiles: 30,
1883
+ couplingCluster: {
1884
+ minCoChangeCommits: 2,
1885
+ percentileThreshold: 0.9,
1886
+ floorScore: 0.35
1887
+ },
1888
+ amplificationZone: {
1889
+ pressureFloor: 0.2,
1890
+ percentileThreshold: 0.85,
1891
+ maxZones: 20
1892
+ },
1893
+ module: {
1894
+ maxPrefixSegments: 2,
1895
+ rootLabel: "(root)",
1896
+ commonSourceRoots: ["src", "lib", "app", "packages"]
1897
+ },
1898
+ dependencySignals: {
1899
+ inheritedSignalMultiplier: 0.45,
1900
+ // At this age, staleness reaches 50% risk.
1901
+ abandonedHalfLifeDays: 540,
1902
+ missingMetadataPenalty: 0.5
1903
+ },
1904
+ externalDimension: {
1905
+ topDependencyPercentile: 0.85,
1906
+ dependencyDepthHalfLife: 6
1907
+ }
1908
+ };
1909
+ var clamp01 = (value) => {
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
+ };
1921
+ var round44 = (value) => Number(value.toFixed(4));
1922
+ var average = (values) => {
1923
+ if (values.length === 0) {
1924
+ return 0;
1925
+ }
1926
+ const total = values.reduce((sum, current) => sum + current, 0);
1927
+ return total / values.length;
1928
+ };
1929
+ var percentile = (values, p) => {
1930
+ if (values.length === 0) {
1931
+ return 0;
1932
+ }
1933
+ if (values.length === 1) {
1934
+ return values[0] ?? 0;
1935
+ }
1936
+ const sorted = [...values].sort((a, b) => a - b);
1937
+ const position = clamp01(p) * (sorted.length - 1);
1938
+ const lowerIndex = Math.floor(position);
1939
+ const upperIndex = Math.ceil(position);
1940
+ const lower = sorted[lowerIndex] ?? 0;
1941
+ const upper = sorted[upperIndex] ?? lower;
1942
+ if (lowerIndex === upperIndex) {
1943
+ return lower;
1944
+ }
1945
+ const ratio = position - lowerIndex;
1946
+ return lower + (upper - lower) * ratio;
1947
+ };
1948
+ var saturatingComposite = (baseline, amplifications) => {
1949
+ let value = clamp01(baseline);
1950
+ for (const amplification of amplifications) {
1951
+ const boundedAmplification = clamp01(amplification);
1952
+ value += (1 - value) * boundedAmplification;
1953
+ }
1954
+ return clamp01(value);
1955
+ };
1956
+ var halfLifeRisk = (value, halfLife) => {
1957
+ if (value <= 0 || halfLife <= 0) {
1958
+ return 0;
1959
+ }
1960
+ return clamp01(value / (value + halfLife));
1961
+ };
1962
+ var normalizeWeights = (weights, enabled) => {
1963
+ let total = 0;
1964
+ const result = { ...weights };
1965
+ for (const key of Object.keys(result)) {
1966
+ const enabledValue = enabled[key];
1967
+ if (!enabledValue) {
1968
+ result[key] = 0;
1969
+ continue;
1970
+ }
1971
+ const value = Math.max(0, result[key]);
1972
+ result[key] = value;
1973
+ total += value;
1974
+ }
1975
+ if (total === 0) {
1976
+ const activeKeys = Object.keys(result).filter((key) => enabled[key]);
1977
+ if (activeKeys.length === 0) {
1978
+ return result;
1979
+ }
1980
+ const uniform = 1 / activeKeys.length;
1981
+ for (const key of activeKeys) {
1982
+ result[key] = uniform;
1983
+ }
1984
+ return result;
1985
+ }
1986
+ for (const key of Object.keys(result)) {
1987
+ if (enabled[key]) {
1988
+ result[key] = result[key] / total;
1989
+ }
1990
+ }
1991
+ return result;
1992
+ };
1993
+ var logScale = (value) => Math.log1p(Math.max(0, value));
1994
+ var buildQuantileScale = (values, lowerPercentile, upperPercentile) => {
1995
+ if (values.length === 0) {
1996
+ return { lower: 0, upper: 0 };
1997
+ }
1998
+ return {
1999
+ lower: percentile(values, lowerPercentile),
2000
+ upper: percentile(values, upperPercentile)
2001
+ };
2002
+ };
2003
+ var normalizeWithScale = (value, scale) => {
2004
+ if (scale.upper <= scale.lower) {
2005
+ return value > 0 ? 1 : 0;
2006
+ }
2007
+ return clamp01((value - scale.lower) / (scale.upper - scale.lower));
2008
+ };
2009
+ var normalizePath2 = (path) => path.replaceAll("\\", "/");
2010
+ var dependencySignalWeights = {
2011
+ single_maintainer: 0.3,
2012
+ abandoned: 0.3,
2013
+ high_centrality: 0.16,
2014
+ deep_chain: 0.14,
2015
+ high_fanout: 0.06,
2016
+ metadata_unavailable: 0.04
2017
+ };
2018
+ var dependencySignalWeightBudget = Object.values(dependencySignalWeights).reduce(
2019
+ (sum, value) => sum + value,
2020
+ 0
2021
+ );
2022
+ var computeDependencySignalScore = (ownSignals, inheritedSignals, inheritedSignalMultiplier) => {
2023
+ const ownWeight = ownSignals.reduce((sum, signal) => sum + (dependencySignalWeights[signal] ?? 0), 0);
2024
+ const inheritedWeight = inheritedSignals.reduce(
2025
+ (sum, signal) => sum + (dependencySignalWeights[signal] ?? 0),
2026
+ 0
2027
+ );
2028
+ const weightedTotal = ownWeight + inheritedWeight * inheritedSignalMultiplier;
2029
+ const maxWeightedTotal = dependencySignalWeightBudget * (1 + inheritedSignalMultiplier);
2030
+ if (maxWeightedTotal <= 0) {
2031
+ return 0;
2032
+ }
2033
+ return clamp01(weightedTotal / maxWeightedTotal);
2034
+ };
2035
+ var computeDependencyScores = (external, config) => {
2036
+ if (!external.available) {
2037
+ return {
2038
+ dependencyScores: [],
2039
+ repositoryExternalPressure: 0
2040
+ };
2041
+ }
2042
+ const transitiveCounts = external.dependencies.map(
2043
+ (dependency) => logScale(dependency.transitiveDependencies.length)
2044
+ );
2045
+ const dependentCounts = external.dependencies.map((dependency) => logScale(dependency.dependents));
2046
+ const chainDepths = external.dependencies.map((dependency) => dependency.dependencyDepth);
2047
+ const transitiveScale = buildQuantileScale(
2048
+ transitiveCounts,
2049
+ config.quantileClamp.lower,
2050
+ config.quantileClamp.upper
2051
+ );
2052
+ const dependentScale = buildQuantileScale(
2053
+ dependentCounts,
2054
+ config.quantileClamp.lower,
2055
+ config.quantileClamp.upper
2056
+ );
2057
+ const chainDepthScale = buildQuantileScale(
2058
+ chainDepths,
2059
+ config.quantileClamp.lower,
2060
+ config.quantileClamp.upper
2061
+ );
2062
+ const dependencyScores = external.dependencies.map((dependency) => {
2063
+ const signalScore = computeDependencySignalScore(
2064
+ dependency.ownRiskSignals,
2065
+ dependency.inheritedRiskSignals,
2066
+ config.dependencySignals.inheritedSignalMultiplier
2067
+ );
2068
+ const maintainerConcentrationRisk = dependency.maintainerCount === null ? config.dependencySignals.missingMetadataPenalty : clamp01(1 / Math.max(1, dependency.maintainerCount));
2069
+ const stalenessRisk = dependency.daysSinceLastRelease === null ? config.dependencySignals.missingMetadataPenalty : halfLifeRisk(
2070
+ dependency.daysSinceLastRelease,
2071
+ config.dependencySignals.abandonedHalfLifeDays
2072
+ );
2073
+ const transitiveBurdenRisk = normalizeWithScale(
2074
+ logScale(dependency.transitiveDependencies.length),
2075
+ transitiveScale
2076
+ );
2077
+ const centralityRisk = normalizeWithScale(logScale(dependency.dependents), dependentScale);
2078
+ const chainDepthRisk = normalizeWithScale(dependency.dependencyDepth, chainDepthScale);
2079
+ const busFactorRisk = dependency.busFactor === null ? config.dependencySignals.missingMetadataPenalty : clamp01(1 / Math.max(1, dependency.busFactor));
2080
+ const weights = config.dependencyFactorWeights;
2081
+ const normalizedScore = clamp01(
2082
+ signalScore * weights.signals + stalenessRisk * weights.staleness + maintainerConcentrationRisk * weights.maintainerConcentration + transitiveBurdenRisk * weights.transitiveBurden + centralityRisk * weights.centrality + chainDepthRisk * weights.chainDepth + busFactorRisk * weights.busFactorRisk
2083
+ );
2084
+ return {
2085
+ dependency: dependency.name,
2086
+ score: round44(normalizedScore * 100),
2087
+ normalizedScore: round44(normalizedScore),
2088
+ ownRiskSignals: dependency.ownRiskSignals,
2089
+ inheritedRiskSignals: dependency.inheritedRiskSignals
2090
+ };
2091
+ }).sort(
2092
+ (a, b) => b.normalizedScore - a.normalizedScore || a.dependency.localeCompare(b.dependency)
2093
+ );
2094
+ const normalizedValues = dependencyScores.map((score) => score.normalizedScore);
2095
+ const highDependencyRisk = dependencyScores.length === 0 ? 0 : percentile(normalizedValues, config.externalDimension.topDependencyPercentile);
2096
+ const averageDependencyRisk = average(normalizedValues);
2097
+ const depthRisk = halfLifeRisk(
2098
+ external.metrics.dependencyDepth,
2099
+ config.externalDimension.dependencyDepthHalfLife
2100
+ );
2101
+ const repositoryExternalPressure = clamp01(
2102
+ highDependencyRisk * 0.5 + averageDependencyRisk * 0.3 + depthRisk * 0.2
2103
+ );
2104
+ return {
2105
+ dependencyScores,
2106
+ repositoryExternalPressure: round44(repositoryExternalPressure)
2107
+ };
2108
+ };
2109
+ var mapEvolutionByFile = (evolution) => {
2110
+ if (!evolution.available) {
2111
+ return /* @__PURE__ */ new Map();
2112
+ }
2113
+ return new Map(
2114
+ evolution.files.map((fileMetrics) => [normalizePath2(fileMetrics.filePath), fileMetrics])
2115
+ );
2116
+ };
2117
+ var computeEvolutionScales = (evolutionByFile, config) => {
2118
+ const evolutionFiles = [...evolutionByFile.values()];
2119
+ return {
2120
+ commitCount: buildQuantileScale(
2121
+ evolutionFiles.map((metrics) => logScale(metrics.commitCount)),
2122
+ config.quantileClamp.lower,
2123
+ config.quantileClamp.upper
2124
+ ),
2125
+ churnTotal: buildQuantileScale(
2126
+ evolutionFiles.map((metrics) => logScale(metrics.churnTotal)),
2127
+ config.quantileClamp.lower,
2128
+ config.quantileClamp.upper
2129
+ ),
2130
+ busFactor: buildQuantileScale(
2131
+ evolutionFiles.map((metrics) => metrics.busFactor),
2132
+ config.quantileClamp.lower,
2133
+ config.quantileClamp.upper
2134
+ )
2135
+ };
2136
+ };
2137
+ var inferModuleName = (filePath, config) => {
2138
+ const normalized = normalizePath2(filePath);
2139
+ const parts = normalized.split("/").filter((part) => part.length > 0);
2140
+ if (parts.length <= 1) {
2141
+ return config.module.rootLabel;
2142
+ }
2143
+ const first = parts[0];
2144
+ if (first === void 0) {
2145
+ return config.module.rootLabel;
2146
+ }
2147
+ if (!config.module.commonSourceRoots.includes(first)) {
2148
+ return first;
2149
+ }
2150
+ if (parts.length <= config.module.maxPrefixSegments) {
2151
+ return first;
2152
+ }
2153
+ return parts.slice(0, config.module.maxPrefixSegments).join("/");
2154
+ };
2155
+ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) => {
2156
+ const clusters = [];
2157
+ let cycleClusterCount = 0;
2158
+ for (const cycle of structural.cycles) {
2159
+ const files = [...new Set(cycle.nodes.map((node) => normalizePath2(node)))].filter(
2160
+ (filePath) => fileScoresByFile.has(filePath)
2161
+ );
2162
+ if (files.length < 2) {
2163
+ continue;
2164
+ }
2165
+ files.sort((a, b) => a.localeCompare(b));
2166
+ const averageRisk = average(
2167
+ files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
2168
+ );
2169
+ const cycleSizeRisk = clamp01((files.length - 1) / 5);
2170
+ const score = round44(clamp01(averageRisk * 0.75 + cycleSizeRisk * 0.25) * 100);
2171
+ cycleClusterCount += 1;
2172
+ clusters.push({
2173
+ id: `cycle:${cycleClusterCount}`,
2174
+ kind: "structural_cycle",
2175
+ files,
2176
+ score
2177
+ });
2178
+ }
2179
+ if (evolution.available && evolution.coupling.pairs.length > 0) {
2180
+ const candidates = evolution.coupling.pairs.filter(
2181
+ (pair) => pair.coChangeCommits >= config.couplingCluster.minCoChangeCommits
2182
+ );
2183
+ const threshold = Math.max(
2184
+ config.couplingCluster.floorScore,
2185
+ percentile(
2186
+ candidates.map((pair) => pair.couplingScore),
2187
+ config.couplingCluster.percentileThreshold
2188
+ )
2189
+ );
2190
+ const selectedPairs = candidates.filter((pair) => pair.couplingScore >= threshold).map((pair) => ({
2191
+ fileA: normalizePath2(pair.fileA),
2192
+ fileB: normalizePath2(pair.fileB),
2193
+ couplingScore: pair.couplingScore
2194
+ })).filter(
2195
+ (pair) => pair.fileA !== pair.fileB && fileScoresByFile.has(pair.fileA) && fileScoresByFile.has(pair.fileB)
2196
+ );
2197
+ const adjacency = /* @__PURE__ */ new Map();
2198
+ for (const pair of selectedPairs) {
2199
+ const aNeighbors = adjacency.get(pair.fileA) ?? /* @__PURE__ */ new Set();
2200
+ aNeighbors.add(pair.fileB);
2201
+ adjacency.set(pair.fileA, aNeighbors);
2202
+ const bNeighbors = adjacency.get(pair.fileB) ?? /* @__PURE__ */ new Set();
2203
+ bNeighbors.add(pair.fileA);
2204
+ adjacency.set(pair.fileB, bNeighbors);
2205
+ }
2206
+ const visited = /* @__PURE__ */ new Set();
2207
+ let couplingClusterCount = 0;
2208
+ const orderedStarts = [...adjacency.keys()].sort((a, b) => a.localeCompare(b));
2209
+ for (const start of orderedStarts) {
2210
+ if (visited.has(start)) {
2211
+ continue;
2212
+ }
2213
+ const stack = [start];
2214
+ const files = [];
2215
+ while (stack.length > 0) {
2216
+ const current = stack.pop();
2217
+ if (current === void 0 || visited.has(current)) {
2218
+ continue;
2219
+ }
2220
+ visited.add(current);
2221
+ files.push(current);
2222
+ const neighbors = adjacency.get(current);
2223
+ if (neighbors === void 0) {
2224
+ continue;
2225
+ }
2226
+ for (const neighbor of neighbors) {
2227
+ if (!visited.has(neighbor)) {
2228
+ stack.push(neighbor);
2229
+ }
2230
+ }
2231
+ }
2232
+ if (files.length < 2) {
2233
+ continue;
2234
+ }
2235
+ files.sort((a, b) => a.localeCompare(b));
2236
+ const fileSet = new Set(files);
2237
+ const componentPairs = selectedPairs.filter(
2238
+ (pair) => fileSet.has(pair.fileA) && fileSet.has(pair.fileB)
2239
+ );
2240
+ const meanFileRisk = average(
2241
+ files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
2242
+ );
2243
+ const meanCoupling = average(componentPairs.map((pair) => pair.couplingScore));
2244
+ const score = round44(clamp01(meanFileRisk * 0.65 + meanCoupling * 0.35) * 100);
2245
+ couplingClusterCount += 1;
2246
+ clusters.push({
2247
+ id: `coupling:${couplingClusterCount}`,
2248
+ kind: "change_coupling",
2249
+ files,
2250
+ score
2251
+ });
2252
+ }
2253
+ }
2254
+ return clusters.sort(
2255
+ (a, b) => b.score - a.score || a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)
2256
+ );
2257
+ };
2258
+ var computeRiskSummary = (structural, evolution, external, config) => {
2259
+ const dependencyComputation = computeDependencyScores(external, config);
2260
+ const evolutionByFile = mapEvolutionByFile(evolution);
2261
+ const evolutionScales = computeEvolutionScales(evolutionByFile, config);
2262
+ const cycleFileSet = new Set(
2263
+ structural.cycles.flatMap((cycle) => cycle.nodes.map((node) => normalizePath2(node)))
2264
+ );
2265
+ const fanInScale = buildQuantileScale(
2266
+ structural.files.map((file) => logScale(file.fanIn)),
2267
+ config.quantileClamp.lower,
2268
+ config.quantileClamp.upper
2269
+ );
2270
+ const fanOutScale = buildQuantileScale(
2271
+ structural.files.map((file) => logScale(file.fanOut)),
2272
+ config.quantileClamp.lower,
2273
+ config.quantileClamp.upper
2274
+ );
2275
+ const depthScale = buildQuantileScale(
2276
+ structural.files.map((file) => file.depth),
2277
+ config.quantileClamp.lower,
2278
+ config.quantileClamp.upper
2279
+ );
2280
+ const dimensionWeights = normalizeWeights(config.dimensionWeights, {
2281
+ structural: true,
2282
+ evolution: evolution.available,
2283
+ external: external.available
2284
+ });
2285
+ const fileRiskContexts = structural.files.map((file) => {
2286
+ const filePath = normalizePath2(file.id);
2287
+ const inCycle = cycleFileSet.has(filePath) ? 1 : 0;
2288
+ const fanInRisk = normalizeWithScale(logScale(file.fanIn), fanInScale);
2289
+ const fanOutRisk = normalizeWithScale(logScale(file.fanOut), fanOutScale);
2290
+ const depthRisk = normalizeWithScale(file.depth, depthScale);
2291
+ const structuralWeights = config.structuralFactorWeights;
2292
+ const structuralFactor = clamp01(
2293
+ fanInRisk * structuralWeights.fanIn + fanOutRisk * structuralWeights.fanOut + depthRisk * structuralWeights.depth + inCycle * structuralWeights.cycleParticipation
2294
+ );
2295
+ const structuralCentrality = clamp01((fanInRisk + fanOutRisk) / 2);
2296
+ let evolutionFactor = 0;
2297
+ const evolutionMetrics = evolutionByFile.get(filePath);
2298
+ if (evolution.available && evolutionMetrics !== void 0) {
2299
+ const frequencyRisk = normalizeWithScale(
2300
+ logScale(evolutionMetrics.commitCount),
2301
+ evolutionScales.commitCount
2302
+ );
2303
+ const churnRisk = normalizeWithScale(
2304
+ logScale(evolutionMetrics.churnTotal),
2305
+ evolutionScales.churnTotal
2306
+ );
2307
+ const volatilityRisk = clamp01(evolutionMetrics.recentVolatility);
2308
+ const ownershipConcentrationRisk = clamp01(evolutionMetrics.topAuthorShare);
2309
+ const busFactorRisk = clamp01(1 - normalizeWithScale(evolutionMetrics.busFactor, evolutionScales.busFactor));
2310
+ const evolutionWeights = config.evolutionFactorWeights;
2311
+ evolutionFactor = clamp01(
2312
+ frequencyRisk * evolutionWeights.frequency + churnRisk * evolutionWeights.churn + volatilityRisk * evolutionWeights.recentVolatility + ownershipConcentrationRisk * evolutionWeights.ownershipConcentration + busFactorRisk * evolutionWeights.busFactorRisk
2313
+ );
2314
+ }
2315
+ const dependencyAffinity = clamp01(structuralCentrality * 0.6 + evolutionFactor * 0.4);
2316
+ const externalFactor = external.available ? clamp01(dependencyComputation.repositoryExternalPressure * dependencyAffinity) : 0;
2317
+ const baseline = structuralFactor * dimensionWeights.structural + evolutionFactor * dimensionWeights.evolution + externalFactor * dimensionWeights.external;
2318
+ const interactions = [
2319
+ structuralFactor * evolutionFactor * config.interactionWeights.structuralEvolution,
2320
+ structuralCentrality * evolutionFactor * config.interactionWeights.centralInstability,
2321
+ externalFactor * Math.max(structuralFactor, evolutionFactor) * config.interactionWeights.dependencyAmplification
2322
+ ];
2323
+ const normalizedScore = saturatingComposite(baseline, interactions);
2324
+ return {
2325
+ file: filePath,
2326
+ score: round44(normalizedScore * 100),
2327
+ normalizedScore: round44(normalizedScore),
2328
+ factors: {
2329
+ structural: round44(structuralFactor),
2330
+ evolution: round44(evolutionFactor),
2331
+ external: round44(externalFactor)
2332
+ },
2333
+ structuralCentrality: round44(structuralCentrality)
2334
+ };
2335
+ }).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
2336
+ const fileScores = fileRiskContexts.map((context) => ({
2337
+ file: context.file,
2338
+ score: context.score,
2339
+ normalizedScore: context.normalizedScore,
2340
+ factors: context.factors
2341
+ }));
2342
+ const fileScoresByFile = new Map(fileScores.map((fileScore) => [fileScore.file, fileScore]));
2343
+ const hotspotsCount = Math.min(
2344
+ config.hotspotMaxFiles,
2345
+ Math.max(config.hotspotMinFiles, Math.ceil(fileScores.length * config.hotspotTopPercent))
2346
+ );
2347
+ const hotspots = fileScores.slice(0, hotspotsCount).map((fileScore) => ({
2348
+ file: fileScore.file,
2349
+ score: fileScore.score,
2350
+ factors: fileScore.factors
2351
+ }));
2352
+ const moduleFiles = /* @__PURE__ */ new Map();
2353
+ for (const fileScore of fileScores) {
2354
+ const moduleName = inferModuleName(fileScore.file, config);
2355
+ const values = moduleFiles.get(moduleName) ?? [];
2356
+ values.push(fileScore.normalizedScore);
2357
+ moduleFiles.set(moduleName, values);
2358
+ }
2359
+ const moduleScores = [...moduleFiles.entries()].map(([module, values]) => {
2360
+ const averageScore = average(values);
2361
+ const peakScore = values.reduce((max, value) => Math.max(max, value), 0);
2362
+ const normalizedScore = clamp01(averageScore * 0.65 + peakScore * 0.35);
2363
+ return {
2364
+ module,
2365
+ score: round44(normalizedScore * 100),
2366
+ normalizedScore: round44(normalizedScore),
2367
+ fileCount: values.length
2368
+ };
2369
+ }).sort((a, b) => b.score - a.score || a.module.localeCompare(b.module));
2370
+ const fragileClusters = buildFragileClusters(structural, evolution, fileScoresByFile, config);
2371
+ const externalPressures = fileScores.map((fileScore) => fileScore.factors.external);
2372
+ const pressureThreshold = Math.max(
2373
+ config.amplificationZone.pressureFloor,
2374
+ percentile(externalPressures, config.amplificationZone.percentileThreshold)
2375
+ );
2376
+ const dependencyAmplificationZones = fileScores.map((fileScore) => {
2377
+ const intensity = clamp01(
2378
+ fileScore.factors.external * Math.max(fileScore.factors.structural, fileScore.factors.evolution)
2379
+ );
2380
+ const normalizedZoneScore = clamp01(intensity * 0.7 + fileScore.normalizedScore * 0.3);
2381
+ return {
2382
+ file: fileScore.file,
2383
+ score: round44(normalizedZoneScore * 100),
2384
+ externalPressure: fileScore.factors.external
2385
+ };
2386
+ }).filter((zone) => external.available && zone.externalPressure >= pressureThreshold).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file)).slice(0, config.amplificationZone.maxZones).map((zone) => ({
2387
+ ...zone,
2388
+ externalPressure: round44(zone.externalPressure)
2389
+ }));
2390
+ const structuralDimension = average(fileScores.map((fileScore) => fileScore.factors.structural));
2391
+ const evolutionDimension = average(fileScores.map((fileScore) => fileScore.factors.evolution));
2392
+ const externalDimension = dependencyComputation.repositoryExternalPressure;
2393
+ const topCentralSlice = Math.max(1, Math.ceil(fileRiskContexts.length * 0.1));
2394
+ const criticalInstability = average(
2395
+ [...fileRiskContexts].sort(
2396
+ (a, b) => b.structuralCentrality * b.factors.evolution - a.structuralCentrality * a.factors.evolution || a.file.localeCompare(b.file)
2397
+ ).slice(0, topCentralSlice).map((context) => context.structuralCentrality * context.factors.evolution)
2398
+ );
2399
+ const dependencyAmplification = average(
2400
+ dependencyAmplificationZones.map(
2401
+ (zone) => clamp01(zone.externalPressure * zone.score / 100)
2402
+ )
2403
+ );
2404
+ const repositoryBaseline = structuralDimension * dimensionWeights.structural + evolutionDimension * dimensionWeights.evolution + externalDimension * dimensionWeights.external;
2405
+ const repositoryNormalizedScore = saturatingComposite(repositoryBaseline, [
2406
+ structuralDimension * evolutionDimension * config.interactionWeights.structuralEvolution,
2407
+ criticalInstability * config.interactionWeights.centralInstability,
2408
+ dependencyAmplification * config.interactionWeights.dependencyAmplification
2409
+ ]);
2410
+ return {
2411
+ repositoryScore: round44(repositoryNormalizedScore * 100),
2412
+ normalizedScore: round44(repositoryNormalizedScore),
2413
+ hotspots,
2414
+ fragileClusters,
2415
+ dependencyAmplificationZones,
2416
+ fileScores,
2417
+ moduleScores,
2418
+ dependencyScores: dependencyComputation.dependencyScores
2419
+ };
2420
+ };
2421
+ var mergeConfig = (overrides) => {
2422
+ if (overrides === void 0) {
2423
+ return DEFAULT_RISK_ENGINE_CONFIG;
2424
+ }
2425
+ return {
2426
+ ...DEFAULT_RISK_ENGINE_CONFIG,
2427
+ ...overrides,
2428
+ dimensionWeights: {
2429
+ ...DEFAULT_RISK_ENGINE_CONFIG.dimensionWeights,
2430
+ ...overrides.dimensionWeights
2431
+ },
2432
+ interactionWeights: {
2433
+ ...DEFAULT_RISK_ENGINE_CONFIG.interactionWeights,
2434
+ ...overrides.interactionWeights
2435
+ },
2436
+ structuralFactorWeights: {
2437
+ ...DEFAULT_RISK_ENGINE_CONFIG.structuralFactorWeights,
2438
+ ...overrides.structuralFactorWeights
2439
+ },
2440
+ evolutionFactorWeights: {
2441
+ ...DEFAULT_RISK_ENGINE_CONFIG.evolutionFactorWeights,
2442
+ ...overrides.evolutionFactorWeights
2443
+ },
2444
+ dependencyFactorWeights: {
2445
+ ...DEFAULT_RISK_ENGINE_CONFIG.dependencyFactorWeights,
2446
+ ...overrides.dependencyFactorWeights
2447
+ },
2448
+ quantileClamp: {
2449
+ ...DEFAULT_RISK_ENGINE_CONFIG.quantileClamp,
2450
+ ...overrides.quantileClamp
2451
+ },
2452
+ couplingCluster: {
2453
+ ...DEFAULT_RISK_ENGINE_CONFIG.couplingCluster,
2454
+ ...overrides.couplingCluster
2455
+ },
2456
+ amplificationZone: {
2457
+ ...DEFAULT_RISK_ENGINE_CONFIG.amplificationZone,
2458
+ ...overrides.amplificationZone
2459
+ },
2460
+ module: {
2461
+ ...DEFAULT_RISK_ENGINE_CONFIG.module,
2462
+ ...overrides.module
2463
+ },
2464
+ dependencySignals: {
2465
+ ...DEFAULT_RISK_ENGINE_CONFIG.dependencySignals,
2466
+ ...overrides.dependencySignals
2467
+ },
2468
+ externalDimension: {
2469
+ ...DEFAULT_RISK_ENGINE_CONFIG.externalDimension,
2470
+ ...overrides.externalDimension
2471
+ }
2472
+ };
2473
+ };
2474
+ var computeRepositoryRiskSummary = (input) => {
2475
+ const config = mergeConfig(input.config);
2476
+ return computeRiskSummary(input.structural, input.evolution, input.external, config);
2477
+ };
2478
+
2479
+ // src/application/run-analyze-command.ts
2480
+ var resolveTargetPath = (inputPath, cwd) => resolve2(cwd, inputPath ?? ".");
2481
+ var createExternalProgressReporter = (logger) => {
2482
+ let lastLoggedProgress = 0;
2483
+ return (event) => {
2484
+ switch (event.stage) {
2485
+ case "package_json_loaded":
2486
+ logger.debug("external: package.json loaded");
2487
+ break;
2488
+ case "lockfile_selected":
2489
+ logger.info(`external: lockfile selected (${event.kind})`);
2490
+ break;
2491
+ case "lockfile_parsed":
2492
+ logger.info(
2493
+ `external: parsed ${event.dependencyNodes} locked dependencies (${event.directDependencies} direct)`
2494
+ );
2495
+ break;
2496
+ case "metadata_fetch_started":
2497
+ logger.info(`external: fetching dependency metadata (${event.total} packages)`);
2498
+ break;
2499
+ case "metadata_fetch_progress": {
2500
+ const currentPercent = event.total === 0 ? 100 : Math.floor(event.completed / event.total * 100);
2501
+ if (event.completed === event.total || event.completed === 1 || event.completed - lastLoggedProgress >= 25) {
2502
+ lastLoggedProgress = event.completed;
2503
+ logger.info(
2504
+ `external: metadata progress ${event.completed}/${event.total} (${currentPercent}%)`
2505
+ );
2506
+ logger.debug(`external: last package processed ${event.packageName}`);
2507
+ }
2508
+ break;
2509
+ }
2510
+ case "metadata_fetch_completed":
2511
+ logger.info(`external: metadata fetch completed (${event.total} packages)`);
2512
+ break;
2513
+ case "summary_built":
2514
+ logger.info(
2515
+ `external: summary built (${event.totalDependencies} total, ${event.directDependencies} direct)`
2516
+ );
2517
+ break;
2518
+ }
2519
+ };
2520
+ };
2521
+ var createStructuralProgressReporter = (logger) => {
2522
+ let lastProcessed = 0;
2523
+ return (event) => {
2524
+ switch (event.stage) {
2525
+ case "config_resolved":
2526
+ if (event.usedFallbackScan) {
2527
+ logger.info(
2528
+ `structural: using filesystem scan discovery (tsconfigs=${event.tsconfigCount})`
2529
+ );
2530
+ } else {
2531
+ logger.info(`structural: discovered tsconfig graph (${event.tsconfigCount} configs)`);
2532
+ }
2533
+ break;
2534
+ case "files_discovered":
2535
+ logger.info(`structural: discovered ${event.totalSourceFiles} source files`);
2536
+ break;
2537
+ case "program_created":
2538
+ logger.debug(`structural: TypeScript program created (${event.totalSourceFiles} files)`);
2539
+ break;
2540
+ case "file_processed":
2541
+ if (event.processed === event.total || event.processed === 1 || event.processed - lastProcessed >= 50) {
2542
+ lastProcessed = event.processed;
2543
+ logger.info(`structural: resolved ${event.processed}/${event.total} files`);
2544
+ logger.debug(`structural: last file processed ${event.filePath}`);
2545
+ }
2546
+ break;
2547
+ case "edges_resolved":
2548
+ logger.info(`structural: resolved ${event.totalEdges} dependency edges`);
2549
+ break;
2550
+ }
2551
+ };
2552
+ };
2553
+ var createEvolutionProgressReporter = (logger) => {
2554
+ let lastParsedRecords = 0;
2555
+ return (event) => {
2556
+ switch (event.stage) {
2557
+ case "checking_git_repository":
2558
+ logger.debug("evolution: checking git repository");
2559
+ break;
2560
+ case "not_git_repository":
2561
+ logger.warn("evolution: target path is not a git repository");
2562
+ break;
2563
+ case "loading_commit_history":
2564
+ logger.info("evolution: loading git history");
2565
+ break;
2566
+ case "history":
2567
+ if (event.event.stage === "git_log_received") {
2568
+ logger.info(`evolution: git log loaded (${event.event.bytes} bytes)`);
2569
+ break;
2570
+ }
2571
+ if (event.event.stage === "git_log_parsed") {
2572
+ logger.info(`evolution: parsed ${event.event.commits} commits`);
2573
+ break;
2574
+ }
2575
+ if (event.event.stage === "git_log_parse_progress" && (event.event.parsedRecords === event.event.totalRecords || event.event.parsedRecords === 1 || event.event.parsedRecords - lastParsedRecords >= 500)) {
2576
+ lastParsedRecords = event.event.parsedRecords;
2577
+ const currentPercent = event.event.totalRecords === 0 ? 100 : Math.floor(event.event.parsedRecords / event.event.totalRecords * 100);
2578
+ logger.info(
2579
+ `evolution: parse progress ${event.event.parsedRecords}/${event.event.totalRecords} (${currentPercent}%)`
2580
+ );
2581
+ }
2582
+ break;
2583
+ case "computing_metrics":
2584
+ logger.info("evolution: computing metrics");
2585
+ break;
2586
+ case "analysis_completed":
2587
+ logger.debug(`evolution: analysis completed (available=${event.available})`);
2588
+ break;
2589
+ }
2590
+ };
2591
+ };
2592
+ var runAnalyzeCommand = async (inputPath, authorIdentityMode, logger = createSilentLogger()) => {
2593
+ const invocationCwd = process.env["INIT_CWD"] ?? process.cwd();
2594
+ const targetPath = resolveTargetPath(inputPath, invocationCwd);
2595
+ logger.info(`analyzing repository: ${targetPath}`);
2596
+ logger.info("building structural graph");
2597
+ const structural = buildProjectGraphSummary({
2598
+ projectPath: targetPath,
2599
+ onProgress: createStructuralProgressReporter(logger)
2600
+ });
2601
+ logger.debug(
2602
+ `structural metrics: nodes=${structural.metrics.nodeCount}, edges=${structural.metrics.edgeCount}, cycles=${structural.metrics.cycleCount}`
2603
+ );
2604
+ logger.info(`analyzing git evolution (author identity: ${authorIdentityMode})`);
2605
+ const evolution = analyzeRepositoryEvolutionFromGit({
2606
+ repositoryPath: targetPath,
2607
+ config: { authorIdentityMode }
2608
+ }, createEvolutionProgressReporter(logger));
2609
+ if (evolution.available) {
2610
+ logger.debug(
2611
+ `evolution metrics: commits=${evolution.metrics.totalCommits}, files=${evolution.metrics.totalFiles}, hotspotThreshold=${evolution.metrics.hotspotThresholdCommitCount}`
2612
+ );
2613
+ } else {
2614
+ logger.warn(`evolution analysis unavailable: ${evolution.reason}`);
2615
+ }
2616
+ logger.info("analyzing external dependencies");
2617
+ const external = await analyzeDependencyExposureFromProject(
2618
+ { repositoryPath: targetPath },
2619
+ createExternalProgressReporter(logger)
2620
+ );
2621
+ if (external.available) {
2622
+ logger.debug(
2623
+ `external metrics: total=${external.metrics.totalDependencies}, direct=${external.metrics.directDependencies}, transitive=${external.metrics.transitiveDependencies}`
2624
+ );
2625
+ } else {
2626
+ logger.warn(`external analysis unavailable: ${external.reason}`);
2627
+ }
2628
+ logger.info("computing risk summary");
2629
+ const risk = computeRepositoryRiskSummary({
2630
+ structural,
2631
+ evolution,
2632
+ external
2633
+ });
2634
+ logger.info(`analysis completed (repositoryScore=${risk.repositoryScore})`);
2635
+ const summary = {
2636
+ structural,
2637
+ evolution,
2638
+ external,
2639
+ risk
2640
+ };
2641
+ return summary;
2642
+ };
2643
+
2644
+ // src/index.ts
2645
+ var program = new Command();
2646
+ var packageJsonPath = resolve3(dirname(fileURLToPath(import.meta.url)), "../package.json");
2647
+ var { version } = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
2648
+ program.name("codesentinel").description("Structural and evolutionary risk analysis for TypeScript/JavaScript codebases").version(version);
2649
+ program.command("analyze").argument("[path]", "path to the project to analyze").addOption(
2650
+ new Option(
2651
+ "--author-identity <mode>",
2652
+ "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
2653
+ ).choices(["likely_merge", "strict_email"]).default("likely_merge")
2654
+ ).addOption(
2655
+ new Option(
2656
+ "--log-level <level>",
2657
+ "log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
2658
+ ).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
2659
+ ).addOption(
2660
+ new Option(
2661
+ "--output <mode>",
2662
+ "output mode: summary (default) or json (full analysis object)"
2663
+ ).choices(["summary", "json"]).default("summary")
2664
+ ).option("--json", "shortcut for --output json").action(
2665
+ async (path, options) => {
2666
+ const logger = createStderrLogger(options.logLevel);
2667
+ const summary = await runAnalyzeCommand(path, options.authorIdentity, logger);
2668
+ const outputMode = options.json === true ? "json" : options.output;
2669
+ process.stdout.write(`${formatAnalyzeOutput(summary, outputMode)}
2670
+ `);
2671
+ }
2672
+ );
2673
+ if (process.argv.length <= 2) {
2674
+ program.outputHelp();
2675
+ process.exit(0);
2676
+ }
2677
+ var executablePath = process.argv[0] ?? "";
2678
+ var scriptPath = process.argv[1] ?? "";
2679
+ var argv = process.argv[2] === "--" ? [executablePath, scriptPath, ...process.argv.slice(3)] : process.argv;
2680
+ if (argv.length <= 2) {
2681
+ program.outputHelp();
2682
+ process.exit(0);
2683
+ }
2684
+ await program.parseAsync(argv);
2685
+ //# sourceMappingURL=index.js.map