@getcodesentinel/codesentinel 1.19.1 → 1.20.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 CHANGED
@@ -233,8 +233,9 @@ Notes:
233
233
 
234
234
  - `likely_merge` (default) may merge multiple emails that likely belong to the same person based on repository history.
235
235
  - `strict_email` treats each canonical email as a distinct author, which avoids false merges but can split the same person across multiple emails.
236
- - Git mailmap is enabled (`git log --use-mailmap`). Put `.mailmap` in the repository being analyzed (the `codesentinel analyze [path]` target). Git will then deterministically unify known aliases before CodeSentinel computes `authorDistribution`.
237
- - `authorDistribution` returns whichever identity mode is selected.
236
+ - Git mailmap is enabled (`git log --use-mailmap`). Put `.mailmap` in the repository being analyzed (the `codesentinel analyze [path]` target). Git will then deterministically unify known aliases before CodeSentinel computes ownership distributions.
237
+ - Evolution output includes both `authorDistributionByCommits` and `authorDistributionByChurn`, plus their derived `topAuthorShare...` and `busFactor...` fields.
238
+ - Current scoring continues to use the commit-weighted ownership view (`...ByCommits`).
238
239
  - Logs are emitted to `stderr` and JSON output is written to `stdout`, so CI redirection still works.
239
240
  - You can set a default log level with `CODESENTINEL_LOG_LEVEL` (`silent|error|warn|info|debug`).
240
241
  - At `info`/`debug`, structural, evolution, and dependency stages report progress so long analyses are observable.
package/dist/index.js CHANGED
@@ -4683,6 +4683,26 @@ var finalizeAuthorDistribution = (authorCommits) => {
4683
4683
  share: round44(commits / totalCommits)
4684
4684
  })).sort((a, b) => b.commits - a.commits || a.authorId.localeCompare(b.authorId));
4685
4685
  };
4686
+ var finalizeAuthorChurnDistribution = (authorChurn) => {
4687
+ const entries = [...authorChurn.entries()].map(([authorId, churn]) => {
4688
+ const churnAdded = churn.churnAdded;
4689
+ const churnDeleted = churn.churnDeleted;
4690
+ return {
4691
+ authorId,
4692
+ churnAdded,
4693
+ churnDeleted,
4694
+ churnTotal: churnAdded + churnDeleted
4695
+ };
4696
+ });
4697
+ const totalChurn = entries.reduce((sum, entry) => sum + entry.churnTotal, 0);
4698
+ if (totalChurn === 0) {
4699
+ return [];
4700
+ }
4701
+ return entries.map((entry) => ({
4702
+ ...entry,
4703
+ share: round44(entry.churnTotal / totalChurn)
4704
+ })).sort((a, b) => b.churnTotal - a.churnTotal || a.authorId.localeCompare(b.authorId));
4705
+ };
4686
4706
  var buildCouplingMatrix = (coChangeByPair, fileCommitCount, consideredCommits, skippedLargeCommits, maxCouplingPairs) => {
4687
4707
  const allPairs = [];
4688
4708
  for (const [key, coChangeCommits] of coChangeByPair.entries()) {
@@ -4751,10 +4771,19 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4751
4771
  recentCommitCount: 0,
4752
4772
  churnAdded: 0,
4753
4773
  churnDeleted: 0,
4754
- authors: /* @__PURE__ */ new Map()
4774
+ authorsByCommits: /* @__PURE__ */ new Map(),
4775
+ authorsByChurn: /* @__PURE__ */ new Map()
4755
4776
  };
4756
4777
  current.churnAdded += fileChange.additions;
4757
4778
  current.churnDeleted += fileChange.deletions;
4779
+ const effectiveAuthorId = authorAliasById.get(commit.authorId) ?? commit.authorId;
4780
+ const authorChurn = current.authorsByChurn.get(effectiveAuthorId) ?? {
4781
+ churnAdded: 0,
4782
+ churnDeleted: 0
4783
+ };
4784
+ authorChurn.churnAdded += fileChange.additions;
4785
+ authorChurn.churnDeleted += fileChange.deletions;
4786
+ current.authorsByChurn.set(effectiveAuthorId, authorChurn);
4758
4787
  fileStats.set(fileChange.filePath, current);
4759
4788
  }
4760
4789
  for (const filePath of uniqueFiles) {
@@ -4767,7 +4796,10 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4767
4796
  current.recentCommitCount += 1;
4768
4797
  }
4769
4798
  const effectiveAuthorId = authorAliasById.get(commit.authorId) ?? commit.authorId;
4770
- current.authors.set(effectiveAuthorId, (current.authors.get(effectiveAuthorId) ?? 0) + 1);
4799
+ current.authorsByCommits.set(
4800
+ effectiveAuthorId,
4801
+ (current.authorsByCommits.get(effectiveAuthorId) ?? 0) + 1
4802
+ );
4771
4803
  }
4772
4804
  const orderedFiles = [...uniqueFiles].sort((a, b) => a.localeCompare(b));
4773
4805
  if (orderedFiles.length > 1) {
@@ -4790,8 +4822,10 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4790
4822
  }
4791
4823
  }
4792
4824
  const files = [...fileStats.entries()].map(([filePath, stats]) => {
4793
- const authorDistribution = finalizeAuthorDistribution(stats.authors);
4794
- const topAuthorShare = authorDistribution[0]?.share ?? 0;
4825
+ const authorDistributionByCommits = finalizeAuthorDistribution(stats.authorsByCommits);
4826
+ const authorDistributionByChurn = finalizeAuthorChurnDistribution(stats.authorsByChurn);
4827
+ const topAuthorShareByCommits = authorDistributionByCommits[0]?.share ?? 0;
4828
+ const topAuthorShareByChurn = authorDistributionByChurn[0]?.share ?? 0;
4795
4829
  return {
4796
4830
  filePath,
4797
4831
  commitCount: stats.commitCount,
@@ -4801,9 +4835,18 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4801
4835
  churnTotal: stats.churnAdded + stats.churnDeleted,
4802
4836
  recentCommitCount: stats.recentCommitCount,
4803
4837
  recentVolatility: stats.commitCount === 0 ? 0 : round44(stats.recentCommitCount / stats.commitCount),
4804
- topAuthorShare,
4805
- busFactor: computeBusFactor(authorDistribution, config.busFactorCoverageThreshold),
4806
- authorDistribution
4838
+ topAuthorShareByCommits,
4839
+ busFactorByCommits: computeBusFactor(
4840
+ authorDistributionByCommits,
4841
+ config.busFactorCoverageThreshold
4842
+ ),
4843
+ authorDistributionByCommits,
4844
+ topAuthorShareByChurn,
4845
+ busFactorByChurn: computeBusFactor(
4846
+ authorDistributionByChurn,
4847
+ config.busFactorCoverageThreshold
4848
+ ),
4849
+ authorDistributionByChurn
4807
4850
  };
4808
4851
  }).sort((a, b) => a.filePath.localeCompare(b.filePath));
4809
4852
  const fileCommitCount = new Map(files.map((file) => [file.filePath, file.commitCount]));
@@ -5558,15 +5601,15 @@ var computeRepositoryHealthSummary = (input) => {
5558
5601
  let singleContributorFiles = 0;
5559
5602
  let trackedFiles = 0;
5560
5603
  for (const file of evolutionSourceFiles) {
5561
- if (file.commitCount <= 0 || file.authorDistribution.length === 0) {
5604
+ if (file.commitCount <= 0 || file.authorDistributionByCommits.length === 0) {
5562
5605
  continue;
5563
5606
  }
5564
5607
  trackedFiles += 1;
5565
- const dominantShare = clamp01(file.authorDistribution[0]?.share ?? 0);
5566
- if (file.authorDistribution.length === 1 || dominantShare >= 0.9) {
5608
+ const dominantShare = clamp01(file.authorDistributionByCommits[0]?.share ?? 0);
5609
+ if (file.authorDistributionByCommits.length === 1 || dominantShare >= 0.9) {
5567
5610
  singleContributorFiles += 1;
5568
5611
  }
5569
- for (const author of file.authorDistribution) {
5612
+ for (const author of file.authorDistributionByCommits) {
5570
5613
  const commits = Math.max(0, author.commits);
5571
5614
  if (commits <= 0) {
5572
5615
  continue;
@@ -6329,7 +6372,7 @@ var computeEvolutionScales = (evolutionByFile, config) => {
6329
6372
  config.quantileClamp.upper
6330
6373
  ),
6331
6374
  busFactor: buildQuantileScale(
6332
- evolutionFiles.map((metrics) => metrics.busFactor),
6375
+ evolutionFiles.map((metrics) => metrics.busFactorByCommits),
6333
6376
  config.quantileClamp.lower,
6334
6377
  config.quantileClamp.upper
6335
6378
  )
@@ -6518,9 +6561,9 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
6518
6561
  evolutionScales.churnTotal
6519
6562
  );
6520
6563
  volatilityRisk = toUnitInterval(evolutionMetrics.recentVolatility);
6521
- ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShare);
6564
+ ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShareByCommits);
6522
6565
  busFactorRisk = toUnitInterval(
6523
- 1 - normalizeWithScale(evolutionMetrics.busFactor, evolutionScales.busFactor)
6566
+ 1 - normalizeWithScale(evolutionMetrics.busFactorByCommits, evolutionScales.busFactor)
6524
6567
  );
6525
6568
  const evolutionWeights = config.evolutionFactorWeights;
6526
6569
  evolutionFactor = toUnitInterval(
@@ -6577,8 +6620,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
6577
6620
  commitCount: evolutionMetrics?.commitCount ?? null,
6578
6621
  churnTotal: evolutionMetrics?.churnTotal ?? null,
6579
6622
  recentVolatility: evolutionMetrics?.recentVolatility ?? null,
6580
- topAuthorShare: evolutionMetrics?.topAuthorShare ?? null,
6581
- busFactor: evolutionMetrics?.busFactor ?? null,
6623
+ topAuthorShareByCommits: evolutionMetrics?.topAuthorShareByCommits ?? null,
6624
+ busFactorByCommits: evolutionMetrics?.busFactorByCommits ?? null,
6582
6625
  dependencyAffinity: round46(dependencyAffinity),
6583
6626
  repositoryExternalPressure: round46(dependencyComputation.repositoryExternalPressure),
6584
6627
  structuralAttenuation: round46(structuralAttenuation)
@@ -6646,8 +6689,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
6646
6689
  commitCount: context.rawMetrics.commitCount,
6647
6690
  churnTotal: context.rawMetrics.churnTotal,
6648
6691
  recentVolatility: context.rawMetrics.recentVolatility,
6649
- topAuthorShare: context.rawMetrics.topAuthorShare,
6650
- busFactor: context.rawMetrics.busFactor
6692
+ topAuthorShareByCommits: context.rawMetrics.topAuthorShareByCommits,
6693
+ busFactorByCommits: context.rawMetrics.busFactorByCommits
6651
6694
  },
6652
6695
  normalizedMetrics: {
6653
6696
  frequencyRisk: context.normalizedMetrics.frequencyRisk,