@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/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 round4 = (value) => Number(value.toFixed(4));
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: round4(commits / totalCommits)
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 : round4(coChangeCommits / denominator);
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
- current.authors.set(commit.authorId, (current.authors.get(commit.authorId) ?? 0) + 1);
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 : round4(stats.commitCount / commits.length * 100),
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 : round4(stats.recentCommitCount / stats.commitCount),
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.toLowerCase(),
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({ repositoryPath: targetPath });
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(readFileSync(packageJsonPath, "utf8"));
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").action((path) => {
818
- const output = runAnalyzeCommand(path);
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.parse(process.argv);
1640
+ await program.parseAsync(process.argv);
827
1641
  //# sourceMappingURL=index.js.map