@codesentinel/codesentinel 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/index.js +830 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import { readFileSync } from "fs";
|
|
4
|
+
import { Command, Option } from "commander";
|
|
5
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
6
6
|
import { dirname, resolve as resolve3 } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
|
|
@@ -448,10 +448,794 @@ var buildProjectGraphSummary = (input) => {
|
|
|
448
448
|
return createGraphAnalysisSummary(input.projectPath, graphData);
|
|
449
449
|
};
|
|
450
450
|
|
|
451
|
+
// ../dependency-firewall/dist/index.js
|
|
452
|
+
import { existsSync, readFileSync } from "fs";
|
|
453
|
+
import { join } from "path";
|
|
454
|
+
var round4 = (value) => Number(value.toFixed(4));
|
|
455
|
+
var normalizeNodes = (nodes) => {
|
|
456
|
+
const byName = /* @__PURE__ */ new Map();
|
|
457
|
+
for (const node of nodes) {
|
|
458
|
+
const bucket = byName.get(node.name) ?? [];
|
|
459
|
+
bucket.push(node);
|
|
460
|
+
byName.set(node.name, bucket);
|
|
461
|
+
}
|
|
462
|
+
const normalized = [];
|
|
463
|
+
for (const [name, candidates] of byName.entries()) {
|
|
464
|
+
if (candidates.length === 0) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
candidates.sort((a, b) => b.version.localeCompare(a.version));
|
|
468
|
+
const selected = candidates[0];
|
|
469
|
+
if (selected === void 0) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const deps = selected.dependencies.map((dep) => {
|
|
473
|
+
const at = dep.lastIndexOf("@");
|
|
474
|
+
return at <= 0 ? dep : dep.slice(0, at);
|
|
475
|
+
}).filter((depName) => depName.length > 0).sort((a, b) => a.localeCompare(b));
|
|
476
|
+
normalized.push({
|
|
477
|
+
key: `${name}@${selected.version}`,
|
|
478
|
+
name,
|
|
479
|
+
version: selected.version,
|
|
480
|
+
dependencies: deps
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return normalized.sort((a, b) => a.name.localeCompare(b.name));
|
|
484
|
+
};
|
|
485
|
+
var computeDepths = (nodeByName, directNames) => {
|
|
486
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
487
|
+
const depthByName = /* @__PURE__ */ new Map();
|
|
488
|
+
const compute = (name) => {
|
|
489
|
+
const known = depthByName.get(name);
|
|
490
|
+
if (known !== void 0) {
|
|
491
|
+
return known;
|
|
492
|
+
}
|
|
493
|
+
if (visiting.has(name)) {
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
visiting.add(name);
|
|
497
|
+
const node = nodeByName.get(name);
|
|
498
|
+
if (node === void 0) {
|
|
499
|
+
visiting.delete(name);
|
|
500
|
+
depthByName.set(name, 0);
|
|
501
|
+
return 0;
|
|
502
|
+
}
|
|
503
|
+
let maxChildDepth = 0;
|
|
504
|
+
for (const dependencyName of node.dependencies) {
|
|
505
|
+
const childDepth = compute(dependencyName);
|
|
506
|
+
if (childDepth > maxChildDepth) {
|
|
507
|
+
maxChildDepth = childDepth;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
visiting.delete(name);
|
|
511
|
+
const ownDepth = directNames.has(name) ? 0 : maxChildDepth + 1;
|
|
512
|
+
depthByName.set(name, ownDepth);
|
|
513
|
+
return ownDepth;
|
|
514
|
+
};
|
|
515
|
+
for (const name of nodeByName.keys()) {
|
|
516
|
+
compute(name);
|
|
517
|
+
}
|
|
518
|
+
let maxDepth = 0;
|
|
519
|
+
for (const depth of depthByName.values()) {
|
|
520
|
+
if (depth > maxDepth) {
|
|
521
|
+
maxDepth = depth;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return { depthByName, maxDepth };
|
|
525
|
+
};
|
|
526
|
+
var rankCentrality = (nodes, dependentsByName, directNames, topN) => [...nodes].map((node) => ({
|
|
527
|
+
name: node.name,
|
|
528
|
+
dependents: dependentsByName.get(node.name) ?? 0,
|
|
529
|
+
fanOut: node.dependencies.length,
|
|
530
|
+
direct: directNames.has(node.name)
|
|
531
|
+
})).sort(
|
|
532
|
+
(a, b) => b.dependents - a.dependents || b.fanOut - a.fanOut || a.name.localeCompare(b.name)
|
|
533
|
+
).slice(0, topN);
|
|
534
|
+
var canPropagateSignal = (signal) => signal === "abandoned" || signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout";
|
|
535
|
+
var collectTransitiveDependencies = (rootName, nodeByName) => {
|
|
536
|
+
const seen = /* @__PURE__ */ new Set();
|
|
537
|
+
const stack = [...nodeByName.get(rootName)?.dependencies ?? []];
|
|
538
|
+
while (stack.length > 0) {
|
|
539
|
+
const current = stack.pop();
|
|
540
|
+
if (current === void 0 || seen.has(current) || current === rootName) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
seen.add(current);
|
|
544
|
+
const currentNode = nodeByName.get(current);
|
|
545
|
+
if (currentNode === void 0) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
for (const next of currentNode.dependencies) {
|
|
549
|
+
if (!seen.has(next)) {
|
|
550
|
+
stack.push(next);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return [...seen].sort((a, b) => a.localeCompare(b));
|
|
555
|
+
};
|
|
556
|
+
var buildExternalAnalysisSummary = (targetPath, extraction, metadataByKey, config) => {
|
|
557
|
+
const nodes = normalizeNodes(extraction.nodes);
|
|
558
|
+
const directNames = new Set(extraction.directDependencies.map((dep) => dep.name));
|
|
559
|
+
const directSpecByName = new Map(extraction.directDependencies.map((dep) => [dep.name, dep.requestedRange]));
|
|
560
|
+
const nodeByName = new Map(nodes.map((node) => [node.name, node]));
|
|
561
|
+
const dependentsByName = /* @__PURE__ */ new Map();
|
|
562
|
+
for (const node of nodes) {
|
|
563
|
+
dependentsByName.set(node.name, dependentsByName.get(node.name) ?? 0);
|
|
564
|
+
}
|
|
565
|
+
for (const node of nodes) {
|
|
566
|
+
for (const dependencyName of node.dependencies) {
|
|
567
|
+
if (!nodeByName.has(dependencyName)) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
dependentsByName.set(dependencyName, (dependentsByName.get(dependencyName) ?? 0) + 1);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const { depthByName, maxDepth } = computeDepths(nodeByName, directNames);
|
|
574
|
+
const centralityRanking = rankCentrality(nodes, dependentsByName, directNames, config.centralityTopN);
|
|
575
|
+
const topCentralNames = new Set(
|
|
576
|
+
centralityRanking.slice(0, Math.max(1, Math.ceil(centralityRanking.length * 0.25))).map((entry) => entry.name)
|
|
577
|
+
);
|
|
578
|
+
const allDependencies = [];
|
|
579
|
+
let metadataAvailableCount = 0;
|
|
580
|
+
for (const node of nodes) {
|
|
581
|
+
const metadata = metadataByKey.get(node.key) ?? null;
|
|
582
|
+
if (metadata !== null) {
|
|
583
|
+
metadataAvailableCount += 1;
|
|
584
|
+
}
|
|
585
|
+
const dependencyDepth = depthByName.get(node.name) ?? 0;
|
|
586
|
+
const dependents = dependentsByName.get(node.name) ?? 0;
|
|
587
|
+
const riskSignals = [];
|
|
588
|
+
if ((metadata?.maintainerCount ?? 0) === 1) {
|
|
589
|
+
riskSignals.push("single_maintainer");
|
|
590
|
+
}
|
|
591
|
+
if ((metadata?.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold) {
|
|
592
|
+
riskSignals.push("abandoned");
|
|
593
|
+
}
|
|
594
|
+
if (topCentralNames.has(node.name) && dependents > 0) {
|
|
595
|
+
riskSignals.push("high_centrality");
|
|
596
|
+
}
|
|
597
|
+
if (dependencyDepth >= config.deepChainThreshold) {
|
|
598
|
+
riskSignals.push("deep_chain");
|
|
599
|
+
}
|
|
600
|
+
if (node.dependencies.length >= config.fanOutHighThreshold) {
|
|
601
|
+
riskSignals.push("high_fanout");
|
|
602
|
+
}
|
|
603
|
+
if (metadata === null) {
|
|
604
|
+
riskSignals.push("metadata_unavailable");
|
|
605
|
+
}
|
|
606
|
+
allDependencies.push({
|
|
607
|
+
name: node.name,
|
|
608
|
+
direct: directNames.has(node.name),
|
|
609
|
+
requestedRange: directSpecByName.get(node.name) ?? null,
|
|
610
|
+
resolvedVersion: node.version,
|
|
611
|
+
transitiveDependencies: [],
|
|
612
|
+
dependencyDepth,
|
|
613
|
+
fanOut: node.dependencies.length,
|
|
614
|
+
dependents,
|
|
615
|
+
maintainerCount: metadata?.maintainerCount ?? null,
|
|
616
|
+
releaseFrequencyDays: metadata?.releaseFrequencyDays ?? null,
|
|
617
|
+
daysSinceLastRelease: metadata?.daysSinceLastRelease ?? null,
|
|
618
|
+
repositoryActivity30d: metadata?.repositoryActivity30d ?? null,
|
|
619
|
+
busFactor: metadata?.busFactor ?? null,
|
|
620
|
+
ownRiskSignals: [...riskSignals].sort((a, b) => a.localeCompare(b)),
|
|
621
|
+
inheritedRiskSignals: [],
|
|
622
|
+
riskSignals
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
allDependencies.sort((a, b) => a.name.localeCompare(b.name));
|
|
626
|
+
const allByName = new Map(allDependencies.map((dep) => [dep.name, dep]));
|
|
627
|
+
const dependencies = allDependencies.filter((dep) => dep.direct).map((dep) => {
|
|
628
|
+
const transitiveDependencies = collectTransitiveDependencies(dep.name, nodeByName);
|
|
629
|
+
const inheritedSignals = /* @__PURE__ */ new Set();
|
|
630
|
+
const allSignals = new Set(dep.ownRiskSignals);
|
|
631
|
+
for (const transitiveName of transitiveDependencies) {
|
|
632
|
+
const transitive = allByName.get(transitiveName);
|
|
633
|
+
if (transitive === void 0) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
for (const signal of transitive.riskSignals) {
|
|
637
|
+
if (canPropagateSignal(signal)) {
|
|
638
|
+
inheritedSignals.add(signal);
|
|
639
|
+
allSignals.add(signal);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
...dep,
|
|
645
|
+
transitiveDependencies,
|
|
646
|
+
inheritedRiskSignals: [...inheritedSignals].sort((a, b) => a.localeCompare(b)),
|
|
647
|
+
riskSignals: [...allSignals].sort((a, b) => a.localeCompare(b))
|
|
648
|
+
};
|
|
649
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
650
|
+
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);
|
|
651
|
+
const singleMaintainerDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("single_maintainer")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
652
|
+
const abandonedDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("abandoned")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
653
|
+
return {
|
|
654
|
+
targetPath,
|
|
655
|
+
available: true,
|
|
656
|
+
metrics: {
|
|
657
|
+
totalDependencies: allDependencies.length,
|
|
658
|
+
directDependencies: dependencies.length,
|
|
659
|
+
transitiveDependencies: allDependencies.length - dependencies.length,
|
|
660
|
+
dependencyDepth: maxDepth,
|
|
661
|
+
lockfileKind: extraction.kind,
|
|
662
|
+
metadataCoverage: allDependencies.length === 0 ? 0 : round4(metadataAvailableCount / allDependencies.length)
|
|
663
|
+
},
|
|
664
|
+
dependencies,
|
|
665
|
+
highRiskDependencies,
|
|
666
|
+
singleMaintainerDependencies,
|
|
667
|
+
abandonedDependencies,
|
|
668
|
+
centralityRanking
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
var DEFAULT_EXTERNAL_ANALYSIS_CONFIG = {
|
|
672
|
+
abandonedDaysThreshold: 540,
|
|
673
|
+
deepChainThreshold: 6,
|
|
674
|
+
fanOutHighThreshold: 25,
|
|
675
|
+
centralityTopN: 20,
|
|
676
|
+
maxHighRiskDependencies: 100,
|
|
677
|
+
metadataRequestConcurrency: 8
|
|
678
|
+
};
|
|
679
|
+
var LOCKFILE_CANDIDATES = [
|
|
680
|
+
{ fileName: "pnpm-lock.yaml", kind: "pnpm" },
|
|
681
|
+
{ fileName: "package-lock.json", kind: "npm" },
|
|
682
|
+
{ fileName: "npm-shrinkwrap.json", kind: "npm-shrinkwrap" },
|
|
683
|
+
{ fileName: "yarn.lock", kind: "yarn" },
|
|
684
|
+
{ fileName: "bun.lock", kind: "bun" },
|
|
685
|
+
{ fileName: "bun.lockb", kind: "bun" }
|
|
686
|
+
];
|
|
687
|
+
var loadPackageJson = (repositoryPath) => {
|
|
688
|
+
const packageJsonPath2 = join(repositoryPath, "package.json");
|
|
689
|
+
if (!existsSync(packageJsonPath2)) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
path: packageJsonPath2,
|
|
694
|
+
raw: readFileSync(packageJsonPath2, "utf8")
|
|
695
|
+
};
|
|
696
|
+
};
|
|
697
|
+
var selectLockfile = (repositoryPath) => {
|
|
698
|
+
for (const candidate of LOCKFILE_CANDIDATES) {
|
|
699
|
+
const absolutePath = join(repositoryPath, candidate.fileName);
|
|
700
|
+
if (!existsSync(absolutePath)) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
path: absolutePath,
|
|
705
|
+
kind: candidate.kind,
|
|
706
|
+
raw: readFileSync(absolutePath, "utf8")
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
};
|
|
711
|
+
var parsePackageJson = (raw) => {
|
|
712
|
+
const parsed = JSON.parse(raw);
|
|
713
|
+
const merged = /* @__PURE__ */ new Map();
|
|
714
|
+
for (const block of [
|
|
715
|
+
parsed.dependencies,
|
|
716
|
+
parsed.devDependencies,
|
|
717
|
+
parsed.optionalDependencies,
|
|
718
|
+
parsed.peerDependencies
|
|
719
|
+
]) {
|
|
720
|
+
if (block === void 0) {
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
for (const [name, versionRange] of Object.entries(block)) {
|
|
724
|
+
merged.set(name, versionRange);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return [...merged.entries()].map(([name, requestedRange]) => ({ name, requestedRange })).sort((a, b) => a.name.localeCompare(b.name));
|
|
728
|
+
};
|
|
729
|
+
var parsePackageLock = (raw, directSpecs) => {
|
|
730
|
+
const parsed = JSON.parse(raw);
|
|
731
|
+
const nodes = [];
|
|
732
|
+
if (parsed.packages !== void 0) {
|
|
733
|
+
for (const [packagePath, packageData] of Object.entries(parsed.packages)) {
|
|
734
|
+
if (packagePath.length === 0 || packageData.version === void 0) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const segments = packagePath.split("node_modules/");
|
|
738
|
+
const name = segments[segments.length - 1] ?? "";
|
|
739
|
+
if (name.length === 0) {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const dependencies = Object.entries(packageData.dependencies ?? {}).map(([depName, depRange]) => `${depName}@${String(depRange)}`).sort((a, b) => a.localeCompare(b));
|
|
743
|
+
nodes.push({
|
|
744
|
+
name,
|
|
745
|
+
version: packageData.version,
|
|
746
|
+
dependencies
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
} else if (parsed.dependencies !== void 0) {
|
|
750
|
+
for (const [name, dep] of Object.entries(parsed.dependencies)) {
|
|
751
|
+
if (dep.version === void 0) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const dependencies = Object.entries(dep.dependencies ?? {}).map(([depName, depVersion]) => `${depName}@${String(depVersion)}`).sort((a, b) => a.localeCompare(b));
|
|
755
|
+
nodes.push({
|
|
756
|
+
name,
|
|
757
|
+
version: dep.version,
|
|
758
|
+
dependencies
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
nodes.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
763
|
+
return {
|
|
764
|
+
kind: "npm",
|
|
765
|
+
directDependencies: directSpecs,
|
|
766
|
+
nodes
|
|
767
|
+
};
|
|
768
|
+
};
|
|
769
|
+
var sanitizeValue = (value) => value.replace(/^['"]|['"]$/g, "").trim();
|
|
770
|
+
var parsePackageKey = (rawKey) => {
|
|
771
|
+
const key = sanitizeValue(rawKey.replace(/:$/, ""));
|
|
772
|
+
const withoutSlash = key.startsWith("/") ? key.slice(1) : key;
|
|
773
|
+
const lastAt = withoutSlash.lastIndexOf("@");
|
|
774
|
+
if (lastAt <= 0) {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
const name = withoutSlash.slice(0, lastAt);
|
|
778
|
+
const versionWithPeers = withoutSlash.slice(lastAt + 1);
|
|
779
|
+
const version2 = versionWithPeers.split("(")[0] ?? versionWithPeers;
|
|
780
|
+
if (name.length === 0 || version2.length === 0) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
return { name, version: version2 };
|
|
784
|
+
};
|
|
785
|
+
var parsePnpmLockfile = (raw, directSpecs) => {
|
|
786
|
+
const lines = raw.split("\n");
|
|
787
|
+
let state = "root";
|
|
788
|
+
let currentPackage = null;
|
|
789
|
+
let currentDependencyName = null;
|
|
790
|
+
const dependenciesByNode = /* @__PURE__ */ new Map();
|
|
791
|
+
for (const line of lines) {
|
|
792
|
+
if (line.trim().length === 0 || line.trimStart().startsWith("#")) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (line.startsWith("importers:")) {
|
|
796
|
+
state = "importers";
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (line.startsWith("packages:")) {
|
|
800
|
+
state = "packages";
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (state === "packages" || state === "packageDeps") {
|
|
804
|
+
const packageMatch = line.match(/^\s{2}([^\s].+):\s*$/);
|
|
805
|
+
if (packageMatch !== null) {
|
|
806
|
+
const parsedKey = parsePackageKey(packageMatch[1] ?? "");
|
|
807
|
+
if (parsedKey !== null) {
|
|
808
|
+
currentPackage = `${parsedKey.name}@${parsedKey.version}`;
|
|
809
|
+
dependenciesByNode.set(currentPackage, /* @__PURE__ */ new Set());
|
|
810
|
+
state = "packageDeps";
|
|
811
|
+
currentDependencyName = null;
|
|
812
|
+
}
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (state === "packageDeps" && currentPackage !== null) {
|
|
817
|
+
const depLine = line.match(/^\s{6}([^:\s]+):\s*(.+)$/);
|
|
818
|
+
if (depLine !== null) {
|
|
819
|
+
const depName = sanitizeValue(depLine[1] ?? "");
|
|
820
|
+
const depRef = sanitizeValue(depLine[2] ?? "");
|
|
821
|
+
const depVersion = depRef.split("(")[0] ?? depRef;
|
|
822
|
+
if (depName.length > 0 && depVersion.length > 0) {
|
|
823
|
+
dependenciesByNode.get(currentPackage)?.add(`${depName}@${depVersion}`);
|
|
824
|
+
}
|
|
825
|
+
currentDependencyName = null;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const depBlockLine = line.match(/^\s{6}([^:\s]+):\s*$/);
|
|
829
|
+
if (depBlockLine !== null) {
|
|
830
|
+
currentDependencyName = sanitizeValue(depBlockLine[1] ?? "");
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const depVersionLine = line.match(/^\s{8}version:\s*(.+)$/);
|
|
834
|
+
if (depVersionLine !== null && currentDependencyName !== null) {
|
|
835
|
+
const depRef = sanitizeValue(depVersionLine[1] ?? "");
|
|
836
|
+
const depVersion = depRef.split("(")[0] ?? depRef;
|
|
837
|
+
if (depVersion.length > 0) {
|
|
838
|
+
dependenciesByNode.get(currentPackage)?.add(`${currentDependencyName}@${depVersion}`);
|
|
839
|
+
}
|
|
840
|
+
currentDependencyName = null;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (line.match(/^\s{4}(dependencies|optionalDependencies):\s*$/) !== null) {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const nodes = [...dependenciesByNode.entries()].map(([nodeId, deps]) => {
|
|
849
|
+
const at = nodeId.lastIndexOf("@");
|
|
850
|
+
return {
|
|
851
|
+
name: nodeId.slice(0, at),
|
|
852
|
+
version: nodeId.slice(at + 1),
|
|
853
|
+
dependencies: [...deps].sort((a, b) => a.localeCompare(b))
|
|
854
|
+
};
|
|
855
|
+
}).sort(
|
|
856
|
+
(a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version)
|
|
857
|
+
);
|
|
858
|
+
return {
|
|
859
|
+
kind: "pnpm",
|
|
860
|
+
directDependencies: directSpecs,
|
|
861
|
+
nodes
|
|
862
|
+
};
|
|
863
|
+
};
|
|
864
|
+
var stripQuotes = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
865
|
+
var parseVersionSelector = (selector) => {
|
|
866
|
+
const npmIndex = selector.lastIndexOf("@npm:");
|
|
867
|
+
if (npmIndex >= 0) {
|
|
868
|
+
return selector.slice(npmIndex + 5);
|
|
869
|
+
}
|
|
870
|
+
const lastAt = selector.lastIndexOf("@");
|
|
871
|
+
if (lastAt <= 0) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
return selector.slice(lastAt + 1);
|
|
875
|
+
};
|
|
876
|
+
var parseYarnLock = (raw, directSpecs) => {
|
|
877
|
+
const lines = raw.split("\n");
|
|
878
|
+
const nodes = [];
|
|
879
|
+
let selectors = [];
|
|
880
|
+
let version2 = null;
|
|
881
|
+
let readingDependencies = false;
|
|
882
|
+
let dependencies = [];
|
|
883
|
+
const flushEntry = () => {
|
|
884
|
+
if (selectors.length === 0 || version2 === null) {
|
|
885
|
+
selectors = [];
|
|
886
|
+
version2 = null;
|
|
887
|
+
dependencies = [];
|
|
888
|
+
readingDependencies = false;
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
for (const selector of selectors) {
|
|
892
|
+
const parsedVersion = parseVersionSelector(selector);
|
|
893
|
+
const at = selector.lastIndexOf("@");
|
|
894
|
+
const name = at <= 0 ? selector : selector.slice(0, at);
|
|
895
|
+
if (name.length === 0) {
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
nodes.push({
|
|
899
|
+
name,
|
|
900
|
+
version: version2,
|
|
901
|
+
dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
|
|
902
|
+
});
|
|
903
|
+
if (parsedVersion !== null) {
|
|
904
|
+
nodes.push({
|
|
905
|
+
name,
|
|
906
|
+
version: parsedVersion,
|
|
907
|
+
dependencies: [...dependencies].sort((a, b) => a.localeCompare(b))
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
selectors = [];
|
|
912
|
+
version2 = null;
|
|
913
|
+
dependencies = [];
|
|
914
|
+
readingDependencies = false;
|
|
915
|
+
};
|
|
916
|
+
for (const line of lines) {
|
|
917
|
+
if (line.trim().length === 0) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (!line.startsWith(" ") && line.endsWith(":")) {
|
|
921
|
+
flushEntry();
|
|
922
|
+
const keyText = line.slice(0, -1);
|
|
923
|
+
selectors = keyText.split(",").map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (line.match(/^\s{2}version\s+/) !== null) {
|
|
927
|
+
const value = line.replace(/^\s{2}version\s+/, "").trim();
|
|
928
|
+
version2 = stripQuotes(value);
|
|
929
|
+
readingDependencies = false;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (line.match(/^\s{2}dependencies:\s*$/) !== null) {
|
|
933
|
+
readingDependencies = true;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (readingDependencies && line.match(/^\s{4}[^\s].+$/) !== null) {
|
|
937
|
+
const depLine = line.trim();
|
|
938
|
+
const firstSpace = depLine.indexOf(" ");
|
|
939
|
+
if (firstSpace <= 0) {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
const depName = stripQuotes(depLine.slice(0, firstSpace));
|
|
943
|
+
const depRef = stripQuotes(depLine.slice(firstSpace + 1).trim());
|
|
944
|
+
const depVersion = parseVersionSelector(depRef) ?? depRef;
|
|
945
|
+
dependencies.push(`${depName}@${depVersion}`);
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
readingDependencies = false;
|
|
949
|
+
}
|
|
950
|
+
flushEntry();
|
|
951
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
952
|
+
for (const node of nodes) {
|
|
953
|
+
const key = `${node.name}@${node.version}`;
|
|
954
|
+
if (!deduped.has(key)) {
|
|
955
|
+
deduped.set(key, node);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
kind: "yarn",
|
|
960
|
+
directDependencies: directSpecs,
|
|
961
|
+
nodes: [...deduped.values()].sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version))
|
|
962
|
+
};
|
|
963
|
+
};
|
|
964
|
+
var parseBunLock = (_raw, _directSpecs) => {
|
|
965
|
+
throw new Error("unsupported_lockfile_format");
|
|
966
|
+
};
|
|
967
|
+
var withDefaults = (overrides) => ({
|
|
968
|
+
...DEFAULT_EXTERNAL_ANALYSIS_CONFIG,
|
|
969
|
+
...overrides
|
|
970
|
+
});
|
|
971
|
+
var parseExtraction = (lockfileKind, lockfileRaw, directSpecs) => {
|
|
972
|
+
switch (lockfileKind) {
|
|
973
|
+
case "pnpm":
|
|
974
|
+
return parsePnpmLockfile(lockfileRaw, directSpecs);
|
|
975
|
+
case "npm":
|
|
976
|
+
case "npm-shrinkwrap":
|
|
977
|
+
return {
|
|
978
|
+
...parsePackageLock(lockfileRaw, directSpecs),
|
|
979
|
+
kind: lockfileKind
|
|
980
|
+
};
|
|
981
|
+
case "yarn":
|
|
982
|
+
return parseYarnLock(lockfileRaw, directSpecs);
|
|
983
|
+
case "bun":
|
|
984
|
+
return parseBunLock(lockfileRaw, directSpecs);
|
|
985
|
+
default:
|
|
986
|
+
throw new Error("unsupported_lockfile_format");
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
var mapWithConcurrency = async (values, limit, handler) => {
|
|
990
|
+
const effectiveLimit = Math.max(1, limit);
|
|
991
|
+
const results = new Array(values.length);
|
|
992
|
+
let index = 0;
|
|
993
|
+
const workers = Array.from({ length: Math.min(effectiveLimit, values.length) }, async () => {
|
|
994
|
+
for (; ; ) {
|
|
995
|
+
const current = index;
|
|
996
|
+
index += 1;
|
|
997
|
+
if (current >= values.length) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const value = values[current];
|
|
1001
|
+
if (value === void 0) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
results[current] = await handler(value);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
await Promise.all(workers);
|
|
1008
|
+
return results;
|
|
1009
|
+
};
|
|
1010
|
+
var analyzeDependencyExposure = async (input, metadataProvider) => {
|
|
1011
|
+
const packageJson = loadPackageJson(input.repositoryPath);
|
|
1012
|
+
if (packageJson === null) {
|
|
1013
|
+
return {
|
|
1014
|
+
targetPath: input.repositoryPath,
|
|
1015
|
+
available: false,
|
|
1016
|
+
reason: "package_json_not_found"
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
const lockfile = selectLockfile(input.repositoryPath);
|
|
1020
|
+
if (lockfile === null) {
|
|
1021
|
+
return {
|
|
1022
|
+
targetPath: input.repositoryPath,
|
|
1023
|
+
available: false,
|
|
1024
|
+
reason: "lockfile_not_found"
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
try {
|
|
1028
|
+
const directSpecs = parsePackageJson(packageJson.raw);
|
|
1029
|
+
const extraction = parseExtraction(lockfile.kind, lockfile.raw, directSpecs);
|
|
1030
|
+
const config = withDefaults(input.config);
|
|
1031
|
+
const metadataEntries = await mapWithConcurrency(
|
|
1032
|
+
extraction.nodes,
|
|
1033
|
+
config.metadataRequestConcurrency,
|
|
1034
|
+
async (node) => ({
|
|
1035
|
+
key: `${node.name}@${node.version}`,
|
|
1036
|
+
metadata: await metadataProvider.getMetadata(node.name, node.version)
|
|
1037
|
+
})
|
|
1038
|
+
);
|
|
1039
|
+
const metadataByKey = /* @__PURE__ */ new Map();
|
|
1040
|
+
for (const entry of metadataEntries) {
|
|
1041
|
+
metadataByKey.set(entry.key, entry.metadata);
|
|
1042
|
+
}
|
|
1043
|
+
return buildExternalAnalysisSummary(input.repositoryPath, extraction, metadataByKey, config);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
1046
|
+
if (message.includes("unsupported_lockfile_format")) {
|
|
1047
|
+
return {
|
|
1048
|
+
targetPath: input.repositoryPath,
|
|
1049
|
+
available: false,
|
|
1050
|
+
reason: "unsupported_lockfile_format"
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
targetPath: input.repositoryPath,
|
|
1055
|
+
available: false,
|
|
1056
|
+
reason: "invalid_lockfile"
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1061
|
+
var round42 = (value) => Number(value.toFixed(4));
|
|
1062
|
+
var parseDate = (iso) => {
|
|
1063
|
+
if (iso === void 0) {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
const value = Date.parse(iso);
|
|
1067
|
+
return Number.isNaN(value) ? null : value;
|
|
1068
|
+
};
|
|
1069
|
+
var NpmRegistryMetadataProvider = class {
|
|
1070
|
+
cache = /* @__PURE__ */ new Map();
|
|
1071
|
+
async getMetadata(name, version2) {
|
|
1072
|
+
const key = `${name}@${version2}`;
|
|
1073
|
+
if (this.cache.has(key)) {
|
|
1074
|
+
return this.cache.get(key) ?? null;
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
const encodedName = encodeURIComponent(name);
|
|
1078
|
+
const response = await fetch(`https://registry.npmjs.org/${encodedName}`);
|
|
1079
|
+
if (!response.ok) {
|
|
1080
|
+
this.cache.set(key, null);
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
const payload = await response.json();
|
|
1084
|
+
const timeEntries = payload.time ?? {};
|
|
1085
|
+
const publishDates = Object.entries(timeEntries).filter(([tag]) => tag !== "created" && tag !== "modified").map(([, date]) => parseDate(date)).filter((value) => value !== null).sort((a, b) => a - b);
|
|
1086
|
+
const modifiedAt = parseDate(timeEntries["modified"]);
|
|
1087
|
+
const now = Date.now();
|
|
1088
|
+
const daysSinceLastRelease = modifiedAt === null ? null : Math.max(0, round42((now - modifiedAt) / ONE_DAY_MS));
|
|
1089
|
+
let releaseFrequencyDays = null;
|
|
1090
|
+
if (publishDates.length >= 2) {
|
|
1091
|
+
const totalIntervals = publishDates.length - 1;
|
|
1092
|
+
let sum = 0;
|
|
1093
|
+
for (let i = 1; i < publishDates.length; i += 1) {
|
|
1094
|
+
const current = publishDates[i];
|
|
1095
|
+
const previous = publishDates[i - 1];
|
|
1096
|
+
if (current !== void 0 && previous !== void 0) {
|
|
1097
|
+
sum += current - previous;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
releaseFrequencyDays = round42(sum / totalIntervals / ONE_DAY_MS);
|
|
1101
|
+
}
|
|
1102
|
+
const maintainers = payload.maintainers ?? [];
|
|
1103
|
+
const maintainerCount = maintainers.length > 0 ? maintainers.length : null;
|
|
1104
|
+
const metadata = {
|
|
1105
|
+
name,
|
|
1106
|
+
version: version2,
|
|
1107
|
+
maintainerCount,
|
|
1108
|
+
releaseFrequencyDays,
|
|
1109
|
+
daysSinceLastRelease,
|
|
1110
|
+
repositoryActivity30d: null,
|
|
1111
|
+
busFactor: null
|
|
1112
|
+
};
|
|
1113
|
+
this.cache.set(key, metadata);
|
|
1114
|
+
return metadata;
|
|
1115
|
+
} catch {
|
|
1116
|
+
this.cache.set(key, null);
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
var NoopMetadataProvider = class {
|
|
1122
|
+
async getMetadata(_name, _version) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
var analyzeDependencyExposureFromProject = async (input) => {
|
|
1127
|
+
const metadataProvider = process.env["CODESENTINEL_EXTERNAL_METADATA"] === "none" ? new NoopMetadataProvider() : new NpmRegistryMetadataProvider();
|
|
1128
|
+
return analyzeDependencyExposure(input, metadataProvider);
|
|
1129
|
+
};
|
|
1130
|
+
|
|
451
1131
|
// ../git-analyzer/dist/index.js
|
|
452
1132
|
import { execFileSync } from "child_process";
|
|
453
1133
|
var pairKey = (a, b) => `${a}\0${b}`;
|
|
454
|
-
var
|
|
1134
|
+
var round43 = (value) => Number(value.toFixed(4));
|
|
1135
|
+
var normalizeName = (value) => value.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
1136
|
+
var extractEmailStem = (authorId) => {
|
|
1137
|
+
const normalized = authorId.trim().toLowerCase();
|
|
1138
|
+
const githubNoReplyMatch = normalized.match(/^\d+\+([^@]+)@users\.noreply\.github\.com$/);
|
|
1139
|
+
if (githubNoReplyMatch?.[1] !== void 0) {
|
|
1140
|
+
return githubNoReplyMatch[1].replace(/[._+-]/g, "");
|
|
1141
|
+
}
|
|
1142
|
+
const atIndex = normalized.indexOf("@");
|
|
1143
|
+
if (atIndex <= 0) {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
return normalized.slice(0, atIndex).replace(/[._+-]/g, "");
|
|
1147
|
+
};
|
|
1148
|
+
var areNamesCompatible = (left, right) => {
|
|
1149
|
+
if (left.length === 0 || right.length === 0) {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
if (left === right) {
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
if (left.startsWith(`${right} `) || right.startsWith(`${left} `)) {
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
return false;
|
|
1159
|
+
};
|
|
1160
|
+
var chooseCanonicalAuthorId = (profiles) => {
|
|
1161
|
+
const ordered = [...profiles].sort((a, b) => {
|
|
1162
|
+
const aIsNoReply = a.authorId.includes("@users.noreply.github.com");
|
|
1163
|
+
const bIsNoReply = b.authorId.includes("@users.noreply.github.com");
|
|
1164
|
+
if (aIsNoReply !== bIsNoReply) {
|
|
1165
|
+
return aIsNoReply ? 1 : -1;
|
|
1166
|
+
}
|
|
1167
|
+
if (a.commitCount !== b.commitCount) {
|
|
1168
|
+
return b.commitCount - a.commitCount;
|
|
1169
|
+
}
|
|
1170
|
+
return a.authorId.localeCompare(b.authorId);
|
|
1171
|
+
});
|
|
1172
|
+
return ordered[0]?.authorId ?? "";
|
|
1173
|
+
};
|
|
1174
|
+
var buildAuthorAliasMap = (commits) => {
|
|
1175
|
+
const nameCountsByAuthorId = /* @__PURE__ */ new Map();
|
|
1176
|
+
const commitCountByAuthorId = /* @__PURE__ */ new Map();
|
|
1177
|
+
for (const commit of commits) {
|
|
1178
|
+
commitCountByAuthorId.set(commit.authorId, (commitCountByAuthorId.get(commit.authorId) ?? 0) + 1);
|
|
1179
|
+
const normalizedName = normalizeName(commit.authorName);
|
|
1180
|
+
const names = nameCountsByAuthorId.get(commit.authorId) ?? /* @__PURE__ */ new Map();
|
|
1181
|
+
if (normalizedName.length > 0) {
|
|
1182
|
+
names.set(normalizedName, (names.get(normalizedName) ?? 0) + 1);
|
|
1183
|
+
}
|
|
1184
|
+
nameCountsByAuthorId.set(commit.authorId, names);
|
|
1185
|
+
}
|
|
1186
|
+
const profiles = [...commitCountByAuthorId.entries()].map(([authorId, commitCount]) => {
|
|
1187
|
+
const names = nameCountsByAuthorId.get(authorId);
|
|
1188
|
+
const primaryName = names === void 0 ? "" : [...names.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]?.[0] ?? "";
|
|
1189
|
+
const normalizedAuthorId = authorId.toLowerCase();
|
|
1190
|
+
const isBot = normalizedAuthorId.includes("[bot]");
|
|
1191
|
+
return {
|
|
1192
|
+
authorId,
|
|
1193
|
+
commitCount,
|
|
1194
|
+
primaryName,
|
|
1195
|
+
emailStem: isBot ? null : extractEmailStem(authorId),
|
|
1196
|
+
isBot
|
|
1197
|
+
};
|
|
1198
|
+
});
|
|
1199
|
+
const groupsByStem = /* @__PURE__ */ new Map();
|
|
1200
|
+
for (const profile of profiles) {
|
|
1201
|
+
if (profile.emailStem === null || profile.emailStem.length < 4) {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
const current = groupsByStem.get(profile.emailStem) ?? [];
|
|
1205
|
+
current.push(profile);
|
|
1206
|
+
groupsByStem.set(profile.emailStem, current);
|
|
1207
|
+
}
|
|
1208
|
+
const aliasMap = /* @__PURE__ */ new Map();
|
|
1209
|
+
for (const profile of profiles) {
|
|
1210
|
+
aliasMap.set(profile.authorId, profile.authorId);
|
|
1211
|
+
}
|
|
1212
|
+
for (const group of groupsByStem.values()) {
|
|
1213
|
+
if (group.length < 2) {
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
const compatible = [];
|
|
1217
|
+
for (const profile of group) {
|
|
1218
|
+
if (profile.isBot || profile.primaryName.length === 0) {
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
compatible.push(profile);
|
|
1222
|
+
}
|
|
1223
|
+
if (compatible.length < 2) {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const canonical = chooseCanonicalAuthorId(compatible);
|
|
1227
|
+
const canonicalProfile = compatible.find((candidate) => candidate.authorId === canonical);
|
|
1228
|
+
if (canonicalProfile === void 0) {
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
for (const profile of compatible) {
|
|
1232
|
+
if (areNamesCompatible(profile.primaryName, canonicalProfile.primaryName)) {
|
|
1233
|
+
aliasMap.set(profile.authorId, canonical);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return aliasMap;
|
|
1238
|
+
};
|
|
455
1239
|
var computeBusFactor = (authorDistribution, threshold) => {
|
|
456
1240
|
if (authorDistribution.length === 0) {
|
|
457
1241
|
return 0;
|
|
@@ -477,7 +1261,7 @@ var finalizeAuthorDistribution = (authorCommits) => {
|
|
|
477
1261
|
return [...authorCommits.entries()].map(([authorId, commits]) => ({
|
|
478
1262
|
authorId,
|
|
479
1263
|
commits,
|
|
480
|
-
share:
|
|
1264
|
+
share: round43(commits / totalCommits)
|
|
481
1265
|
})).sort((a, b) => b.commits - a.commits || a.authorId.localeCompare(b.authorId));
|
|
482
1266
|
};
|
|
483
1267
|
var buildCouplingMatrix = (coChangeByPair, fileCommitCount, consideredCommits, skippedLargeCommits, maxCouplingPairs) => {
|
|
@@ -490,7 +1274,7 @@ var buildCouplingMatrix = (coChangeByPair, fileCommitCount, consideredCommits, s
|
|
|
490
1274
|
const fileACommits = fileCommitCount.get(fileA) ?? 0;
|
|
491
1275
|
const fileBCommits = fileCommitCount.get(fileB) ?? 0;
|
|
492
1276
|
const denominator = fileACommits + fileBCommits - coChangeCommits;
|
|
493
|
-
const couplingScore = denominator === 0 ? 0 :
|
|
1277
|
+
const couplingScore = denominator === 0 ? 0 : round43(coChangeCommits / denominator);
|
|
494
1278
|
allPairs.push({
|
|
495
1279
|
fileA,
|
|
496
1280
|
fileB,
|
|
@@ -529,6 +1313,7 @@ var selectHotspots = (files, config) => {
|
|
|
529
1313
|
return { hotspots, threshold };
|
|
530
1314
|
};
|
|
531
1315
|
var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
|
|
1316
|
+
const authorAliasById = config.authorIdentityMode === "likely_merge" ? buildAuthorAliasMap(commits) : /* @__PURE__ */ new Map();
|
|
532
1317
|
const fileStats = /* @__PURE__ */ new Map();
|
|
533
1318
|
const coChangeByPair = /* @__PURE__ */ new Map();
|
|
534
1319
|
const headCommitTimestamp = commits.length === 0 ? null : commits[commits.length - 1]?.authoredAtUnix ?? null;
|
|
@@ -559,7 +1344,8 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
|
|
|
559
1344
|
if (commit.authoredAtUnix >= recentWindowStart) {
|
|
560
1345
|
current.recentCommitCount += 1;
|
|
561
1346
|
}
|
|
562
|
-
|
|
1347
|
+
const effectiveAuthorId = authorAliasById.get(commit.authorId) ?? commit.authorId;
|
|
1348
|
+
current.authors.set(effectiveAuthorId, (current.authors.get(effectiveAuthorId) ?? 0) + 1);
|
|
563
1349
|
}
|
|
564
1350
|
const orderedFiles = [...uniqueFiles].sort((a, b) => a.localeCompare(b));
|
|
565
1351
|
if (orderedFiles.length > 1) {
|
|
@@ -587,12 +1373,12 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
|
|
|
587
1373
|
return {
|
|
588
1374
|
filePath,
|
|
589
1375
|
commitCount: stats.commitCount,
|
|
590
|
-
frequencyPer100Commits: commits.length === 0 ? 0 :
|
|
1376
|
+
frequencyPer100Commits: commits.length === 0 ? 0 : round43(stats.commitCount / commits.length * 100),
|
|
591
1377
|
churnAdded: stats.churnAdded,
|
|
592
1378
|
churnDeleted: stats.churnDeleted,
|
|
593
1379
|
churnTotal: stats.churnAdded + stats.churnDeleted,
|
|
594
1380
|
recentCommitCount: stats.recentCommitCount,
|
|
595
|
-
recentVolatility: stats.commitCount === 0 ? 0 :
|
|
1381
|
+
recentVolatility: stats.commitCount === 0 ? 0 : round43(stats.recentCommitCount / stats.commitCount),
|
|
596
1382
|
topAuthorShare,
|
|
597
1383
|
busFactor: computeBusFactor(authorDistribution, config.busFactorCoverageThreshold),
|
|
598
1384
|
authorDistribution
|
|
@@ -624,6 +1410,7 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
|
|
|
624
1410
|
};
|
|
625
1411
|
};
|
|
626
1412
|
var DEFAULT_EVOLUTION_CONFIG = {
|
|
1413
|
+
authorIdentityMode: "likely_merge",
|
|
627
1414
|
recentWindowDays: 30,
|
|
628
1415
|
hotspotTopPercent: 0.1,
|
|
629
1416
|
hotspotMinFiles: 1,
|
|
@@ -682,6 +1469,22 @@ var parseInteger = (value) => {
|
|
|
682
1469
|
}
|
|
683
1470
|
return parsed;
|
|
684
1471
|
};
|
|
1472
|
+
var normalizeAuthorIdentity = (authorName, authorEmail) => {
|
|
1473
|
+
const normalizedName = authorName.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1474
|
+
const normalizedEmail = authorEmail.trim().toLowerCase();
|
|
1475
|
+
if (/\[bot\]/i.test(normalizedName) || /\[bot\]/i.test(normalizedEmail)) {
|
|
1476
|
+
return normalizedEmail.length > 0 ? normalizedEmail : normalizedName;
|
|
1477
|
+
}
|
|
1478
|
+
const githubNoReplyMatch = normalizedEmail.match(/^\d+\+([^@]+)@users\.noreply\.github\.com$/);
|
|
1479
|
+
const githubHandle = githubNoReplyMatch?.[1]?.trim().toLowerCase();
|
|
1480
|
+
if (githubHandle !== void 0 && githubHandle.length > 0) {
|
|
1481
|
+
return `${githubHandle}@users.noreply.github.com`;
|
|
1482
|
+
}
|
|
1483
|
+
if (normalizedEmail.length > 0) {
|
|
1484
|
+
return normalizedEmail;
|
|
1485
|
+
}
|
|
1486
|
+
return normalizedName;
|
|
1487
|
+
};
|
|
685
1488
|
var parseRenamedPath = (pathSpec) => {
|
|
686
1489
|
if (!pathSpec.includes(" => ")) {
|
|
687
1490
|
return pathSpec;
|
|
@@ -747,7 +1550,7 @@ var parseGitLog = (rawLog) => {
|
|
|
747
1550
|
}
|
|
748
1551
|
commits.push({
|
|
749
1552
|
hash,
|
|
750
|
-
authorId: authorEmail
|
|
1553
|
+
authorId: normalizeAuthorIdentity(authorName, authorEmail),
|
|
751
1554
|
authorName,
|
|
752
1555
|
authoredAtUnix,
|
|
753
1556
|
fileChanges
|
|
@@ -781,6 +1584,7 @@ var GitCliHistoryProvider = class {
|
|
|
781
1584
|
"-c",
|
|
782
1585
|
"core.quotepath=false",
|
|
783
1586
|
"log",
|
|
1587
|
+
"--use-mailmap",
|
|
784
1588
|
"--no-merges",
|
|
785
1589
|
"--date=unix",
|
|
786
1590
|
`--pretty=format:${GIT_LOG_FORMAT}`,
|
|
@@ -797,14 +1601,19 @@ var analyzeRepositoryEvolutionFromGit = (input) => {
|
|
|
797
1601
|
|
|
798
1602
|
// src/application/run-analyze-command.ts
|
|
799
1603
|
var resolveTargetPath = (inputPath, cwd) => resolve2(cwd, inputPath ?? ".");
|
|
800
|
-
var runAnalyzeCommand = (inputPath) => {
|
|
1604
|
+
var runAnalyzeCommand = async (inputPath, authorIdentityMode) => {
|
|
801
1605
|
const invocationCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
802
1606
|
const targetPath = resolveTargetPath(inputPath, invocationCwd);
|
|
803
1607
|
const structural = buildProjectGraphSummary({ projectPath: targetPath });
|
|
804
|
-
const evolution = analyzeRepositoryEvolutionFromGit({
|
|
1608
|
+
const evolution = analyzeRepositoryEvolutionFromGit({
|
|
1609
|
+
repositoryPath: targetPath,
|
|
1610
|
+
config: { authorIdentityMode }
|
|
1611
|
+
});
|
|
1612
|
+
const external = await analyzeDependencyExposureFromProject({ repositoryPath: targetPath });
|
|
805
1613
|
const summary = {
|
|
806
1614
|
structural,
|
|
807
|
-
evolution
|
|
1615
|
+
evolution,
|
|
1616
|
+
external
|
|
808
1617
|
};
|
|
809
1618
|
return JSON.stringify(summary, null, 2);
|
|
810
1619
|
};
|
|
@@ -812,10 +1621,15 @@ var runAnalyzeCommand = (inputPath) => {
|
|
|
812
1621
|
// src/index.ts
|
|
813
1622
|
var program = new Command();
|
|
814
1623
|
var packageJsonPath = resolve3(dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
815
|
-
var { version } = JSON.parse(
|
|
1624
|
+
var { version } = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
816
1625
|
program.name("codesentinel").description("Structural and evolutionary risk analysis for TypeScript/JavaScript codebases").version(version);
|
|
817
|
-
program.command("analyze").argument("[path]", "path to the project to analyze").
|
|
818
|
-
|
|
1626
|
+
program.command("analyze").argument("[path]", "path to the project to analyze").addOption(
|
|
1627
|
+
new Option(
|
|
1628
|
+
"--author-identity <mode>",
|
|
1629
|
+
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
1630
|
+
).choices(["likely_merge", "strict_email"]).default("likely_merge")
|
|
1631
|
+
).action(async (path, options) => {
|
|
1632
|
+
const output = await runAnalyzeCommand(path, options.authorIdentity);
|
|
819
1633
|
process.stdout.write(`${output}
|
|
820
1634
|
`);
|
|
821
1635
|
});
|
|
@@ -823,5 +1637,5 @@ if (process.argv.length <= 2) {
|
|
|
823
1637
|
program.outputHelp();
|
|
824
1638
|
process.exit(0);
|
|
825
1639
|
}
|
|
826
|
-
program.
|
|
1640
|
+
await program.parseAsync(process.argv);
|
|
827
1641
|
//# sourceMappingURL=index.js.map
|