@getcodesentinel/codesentinel 1.19.0 → 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
@@ -3702,6 +3702,7 @@ var ANSI2 = {
3702
3702
  cyan: "\x1B[36m",
3703
3703
  green: "\x1B[32m"
3704
3704
  };
3705
+ var PROMPT_PADDING = " ";
3705
3706
  var renderMenu = (currentVersion, actions, selectedIndex) => {
3706
3707
  const optionLabels = actions.map((action, index) => `${index + 1}. ${action.label}`);
3707
3708
  const labelWidth = optionLabels.reduce((max, label) => Math.max(max, label.length), 0);
@@ -3732,6 +3733,46 @@ var hideCursor2 = () => {
3732
3733
  var showCursor2 = () => {
3733
3734
  stderr2.write("\x1B[?25h");
3734
3735
  };
3736
+ var pipeWithPadding = (stream, target, padding) => {
3737
+ if (stream === null) {
3738
+ return;
3739
+ }
3740
+ stream.setEncoding("utf8");
3741
+ let buffer = "";
3742
+ let needsPrefix = true;
3743
+ const writeChunk = (chunk) => {
3744
+ let start = 0;
3745
+ while (start < chunk.length) {
3746
+ if (needsPrefix) {
3747
+ target.write(padding);
3748
+ needsPrefix = false;
3749
+ }
3750
+ const newlineIndex = chunk.indexOf("\n", start);
3751
+ if (newlineIndex === -1) {
3752
+ target.write(chunk.slice(start));
3753
+ return;
3754
+ }
3755
+ target.write(chunk.slice(start, newlineIndex + 1));
3756
+ needsPrefix = true;
3757
+ start = newlineIndex + 1;
3758
+ }
3759
+ };
3760
+ stream.on("data", (chunk) => {
3761
+ buffer += chunk;
3762
+ const lastNewlineIndex = buffer.lastIndexOf("\n");
3763
+ if (lastNewlineIndex === -1) {
3764
+ return;
3765
+ }
3766
+ writeChunk(buffer.slice(0, lastNewlineIndex + 1));
3767
+ buffer = buffer.slice(lastNewlineIndex + 1);
3768
+ });
3769
+ stream.on("end", () => {
3770
+ if (buffer.length > 0) {
3771
+ writeChunk(buffer);
3772
+ buffer = "";
3773
+ }
3774
+ });
3775
+ };
3735
3776
  var promptSelection = async (currentVersion, actions) => {
3736
3777
  if (!stdin2.isTTY || !stderr2.isTTY || typeof stdin2.setRawMode !== "function") {
3737
3778
  return "exit";
@@ -3788,46 +3829,99 @@ var createPrompt = () => createPromisesInterface({
3788
3829
  output: stderr2
3789
3830
  });
3790
3831
  var promptText = async (prompt, label, defaultValue) => {
3791
- const suffix = defaultValue === void 0 ? "" : ` [${defaultValue}]`;
3792
- const answer = await prompt.question(
3793
- `${label}${suffix}: `
3794
- );
3832
+ const suffix = defaultValue === void 0 || defaultValue.length === 0 ? "" : ` [${defaultValue}]`;
3833
+ let answer;
3834
+ try {
3835
+ answer = await prompt.question(
3836
+ `${PROMPT_PADDING}${label}${suffix}: `
3837
+ );
3838
+ } catch (error) {
3839
+ if (error instanceof Error && "code" in error && error.code === "ABORT_ERR") {
3840
+ stderr2.write("\n");
3841
+ return null;
3842
+ }
3843
+ throw error;
3844
+ }
3795
3845
  const trimmed = answer.trim();
3796
3846
  return trimmed.length > 0 ? trimmed : defaultValue ?? "";
3797
3847
  };
3848
+ var renderDependencyRiskPrompt = (errorMessage) => {
3849
+ clearTerminal();
3850
+ stderr2.write(`${PROMPT_PADDING}${ANSI2.bold}Scan dependency risk${ANSI2.reset}
3851
+ `);
3852
+ if (errorMessage !== void 0) {
3853
+ stderr2.write(`
3854
+ ${PROMPT_PADDING}${errorMessage}
3855
+ `);
3856
+ }
3857
+ stderr2.write("\n");
3858
+ };
3798
3859
  var buildDependencyRiskArgs = async () => {
3799
3860
  const prompt = createPrompt();
3800
3861
  try {
3801
- const dependency = await promptText(prompt, "Dependency spec", "");
3802
- if (dependency.length === 0) {
3803
- stderr2.write("A dependency spec is required.\n");
3804
- return null;
3862
+ let errorMessage;
3863
+ while (true) {
3864
+ renderDependencyRiskPrompt(errorMessage);
3865
+ const dependency = await promptText(prompt, "Package name");
3866
+ if (dependency === null) {
3867
+ return { kind: "cancel" };
3868
+ }
3869
+ if (dependency.length === 0) {
3870
+ errorMessage = "A package name is required.";
3871
+ continue;
3872
+ }
3873
+ return {
3874
+ kind: "run",
3875
+ args: ["dependency-risk", dependency]
3876
+ };
3805
3877
  }
3806
- return ["dependency-risk", dependency];
3807
3878
  } finally {
3808
3879
  prompt.close();
3809
3880
  }
3810
3881
  };
3811
3882
  var waitForReturnToMenu = async () => {
3812
- const prompt = createPromisesInterface({
3813
- input: stdin2,
3814
- output: stderr2
3815
- });
3816
- try {
3817
- await prompt.question("Press enter to return to the menu...");
3818
- } finally {
3819
- prompt.close();
3883
+ if (!stdin2.isTTY || !stderr2.isTTY || typeof stdin2.setRawMode !== "function") {
3884
+ return;
3820
3885
  }
3886
+ stderr2.write(`
3887
+ ${PROMPT_PADDING}Press enter to return to the menu...`);
3888
+ await new Promise((resolve6) => {
3889
+ emitKeypressEvents2(stdin2);
3890
+ const previousRawMode = stdin2.isRaw;
3891
+ const cleanup = () => {
3892
+ stdin2.off("keypress", onKeypress);
3893
+ stdin2.pause();
3894
+ stdin2.setRawMode(previousRawMode);
3895
+ showCursor2();
3896
+ stderr2.write("\n");
3897
+ resolve6();
3898
+ };
3899
+ const onKeypress = (_str, key) => {
3900
+ if (key.ctrl === true && key.name === "c") {
3901
+ cleanup();
3902
+ return;
3903
+ }
3904
+ if (key.name === "return" || key.name === "enter") {
3905
+ cleanup();
3906
+ }
3907
+ };
3908
+ hideCursor2();
3909
+ stdin2.on("keypress", onKeypress);
3910
+ stdin2.setRawMode(true);
3911
+ stdin2.resume();
3912
+ });
3821
3913
  };
3822
3914
  var runCliCommand = async (scriptPath2, args) => {
3823
3915
  return await new Promise((resolve6, reject) => {
3824
3916
  const child = spawn2(process.execPath, [...process.execArgv, scriptPath2, ...args], {
3825
- stdio: "inherit",
3917
+ stdio: ["inherit", "pipe", "pipe"],
3826
3918
  env: {
3827
3919
  ...process.env,
3828
3920
  CODESENTINEL_NO_UPDATE_NOTIFIER: "1"
3829
3921
  }
3830
3922
  });
3923
+ pipeWithPadding(child.stdout, stdout, PROMPT_PADDING);
3924
+ pipeWithPadding(child.stderr, stderr2, PROMPT_PADDING);
3831
3925
  child.on("error", (error) => {
3832
3926
  reject(error);
3833
3927
  });
@@ -3838,34 +3932,35 @@ var runCliCommand = async (scriptPath2, args) => {
3838
3932
  };
3839
3933
  var runInteractiveCliMenu = async (input) => {
3840
3934
  if (!stdin2.isTTY || !stderr2.isTTY || !stdout.isTTY) {
3841
- stderr2.write("Interactive menu requires a TTY.\n");
3935
+ stderr2.write(`${PROMPT_PADDING}Interactive menu requires a TTY.
3936
+ `);
3842
3937
  return 1;
3843
3938
  }
3844
3939
  const actions = [
3845
3940
  {
3846
3941
  label: "Run overview",
3847
3942
  description: "combined analyze + explain + report",
3848
- commandBuilder: () => ["run"]
3943
+ commandBuilder: () => ({ kind: "run", args: ["run"] })
3849
3944
  },
3850
3945
  {
3851
3946
  label: "Analyze repository",
3852
3947
  description: "structural and health scoring summary",
3853
- commandBuilder: () => ["analyze"]
3948
+ commandBuilder: () => ({ kind: "run", args: ["analyze"] })
3854
3949
  },
3855
3950
  {
3856
3951
  label: "Explain hotspots",
3857
3952
  description: "top findings in markdown by default",
3858
- commandBuilder: () => ["explain", "--format", "md"]
3953
+ commandBuilder: () => ({ kind: "run", args: ["explain", "--format", "md"] })
3859
3954
  },
3860
3955
  {
3861
3956
  label: "Generate report",
3862
3957
  description: "create a full report for a repository",
3863
- commandBuilder: () => ["report", "--format", "md"]
3958
+ commandBuilder: () => ({ kind: "run", args: ["report", "--format", "md"] })
3864
3959
  },
3865
3960
  {
3866
3961
  label: "Run policy check",
3867
3962
  description: "execute governance gates",
3868
- commandBuilder: () => ["check"]
3963
+ commandBuilder: () => ({ kind: "run", args: ["check"] })
3869
3964
  },
3870
3965
  {
3871
3966
  label: "Scan dependency risk",
@@ -3881,18 +3976,18 @@ var runInteractiveCliMenu = async (input) => {
3881
3976
  }
3882
3977
  const selectedAction = actions[selectedIndex];
3883
3978
  if (selectedAction === void 0) {
3884
- stderr2.write("\n");
3979
+ stderr2.write(`
3980
+ ${PROMPT_PADDING}`);
3885
3981
  return 1;
3886
3982
  }
3887
- const args = await selectedAction.commandBuilder();
3888
- if (args === null) {
3889
- await waitForReturnToMenu();
3983
+ const actionResult = await selectedAction.commandBuilder();
3984
+ if (actionResult.kind === "cancel") {
3890
3985
  continue;
3891
3986
  }
3892
- const exitCode = await runCliCommand(input.scriptPath, args);
3987
+ const exitCode = await runCliCommand(input.scriptPath, actionResult.args);
3893
3988
  if (exitCode !== 0) {
3894
3989
  stderr2.write(`
3895
- Command exited with code ${exitCode}.
3990
+ ${PROMPT_PADDING}Command exited with code ${exitCode}.
3896
3991
  `);
3897
3992
  } else {
3898
3993
  stderr2.write("\n");
@@ -4588,6 +4683,26 @@ var finalizeAuthorDistribution = (authorCommits) => {
4588
4683
  share: round44(commits / totalCommits)
4589
4684
  })).sort((a, b) => b.commits - a.commits || a.authorId.localeCompare(b.authorId));
4590
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
+ };
4591
4706
  var buildCouplingMatrix = (coChangeByPair, fileCommitCount, consideredCommits, skippedLargeCommits, maxCouplingPairs) => {
4592
4707
  const allPairs = [];
4593
4708
  for (const [key, coChangeCommits] of coChangeByPair.entries()) {
@@ -4656,10 +4771,19 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4656
4771
  recentCommitCount: 0,
4657
4772
  churnAdded: 0,
4658
4773
  churnDeleted: 0,
4659
- authors: /* @__PURE__ */ new Map()
4774
+ authorsByCommits: /* @__PURE__ */ new Map(),
4775
+ authorsByChurn: /* @__PURE__ */ new Map()
4660
4776
  };
4661
4777
  current.churnAdded += fileChange.additions;
4662
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);
4663
4787
  fileStats.set(fileChange.filePath, current);
4664
4788
  }
4665
4789
  for (const filePath of uniqueFiles) {
@@ -4672,7 +4796,10 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4672
4796
  current.recentCommitCount += 1;
4673
4797
  }
4674
4798
  const effectiveAuthorId = authorAliasById.get(commit.authorId) ?? commit.authorId;
4675
- current.authors.set(effectiveAuthorId, (current.authors.get(effectiveAuthorId) ?? 0) + 1);
4799
+ current.authorsByCommits.set(
4800
+ effectiveAuthorId,
4801
+ (current.authorsByCommits.get(effectiveAuthorId) ?? 0) + 1
4802
+ );
4676
4803
  }
4677
4804
  const orderedFiles = [...uniqueFiles].sort((a, b) => a.localeCompare(b));
4678
4805
  if (orderedFiles.length > 1) {
@@ -4695,8 +4822,10 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4695
4822
  }
4696
4823
  }
4697
4824
  const files = [...fileStats.entries()].map(([filePath, stats]) => {
4698
- const authorDistribution = finalizeAuthorDistribution(stats.authors);
4699
- 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;
4700
4829
  return {
4701
4830
  filePath,
4702
4831
  commitCount: stats.commitCount,
@@ -4706,9 +4835,18 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
4706
4835
  churnTotal: stats.churnAdded + stats.churnDeleted,
4707
4836
  recentCommitCount: stats.recentCommitCount,
4708
4837
  recentVolatility: stats.commitCount === 0 ? 0 : round44(stats.recentCommitCount / stats.commitCount),
4709
- topAuthorShare,
4710
- busFactor: computeBusFactor(authorDistribution, config.busFactorCoverageThreshold),
4711
- 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
4712
4850
  };
4713
4851
  }).sort((a, b) => a.filePath.localeCompare(b.filePath));
4714
4852
  const fileCommitCount = new Map(files.map((file) => [file.filePath, file.commitCount]));
@@ -5463,15 +5601,15 @@ var computeRepositoryHealthSummary = (input) => {
5463
5601
  let singleContributorFiles = 0;
5464
5602
  let trackedFiles = 0;
5465
5603
  for (const file of evolutionSourceFiles) {
5466
- if (file.commitCount <= 0 || file.authorDistribution.length === 0) {
5604
+ if (file.commitCount <= 0 || file.authorDistributionByCommits.length === 0) {
5467
5605
  continue;
5468
5606
  }
5469
5607
  trackedFiles += 1;
5470
- const dominantShare = clamp01(file.authorDistribution[0]?.share ?? 0);
5471
- 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) {
5472
5610
  singleContributorFiles += 1;
5473
5611
  }
5474
- for (const author of file.authorDistribution) {
5612
+ for (const author of file.authorDistributionByCommits) {
5475
5613
  const commits = Math.max(0, author.commits);
5476
5614
  if (commits <= 0) {
5477
5615
  continue;
@@ -6234,7 +6372,7 @@ var computeEvolutionScales = (evolutionByFile, config) => {
6234
6372
  config.quantileClamp.upper
6235
6373
  ),
6236
6374
  busFactor: buildQuantileScale(
6237
- evolutionFiles.map((metrics) => metrics.busFactor),
6375
+ evolutionFiles.map((metrics) => metrics.busFactorByCommits),
6238
6376
  config.quantileClamp.lower,
6239
6377
  config.quantileClamp.upper
6240
6378
  )
@@ -6423,9 +6561,9 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
6423
6561
  evolutionScales.churnTotal
6424
6562
  );
6425
6563
  volatilityRisk = toUnitInterval(evolutionMetrics.recentVolatility);
6426
- ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShare);
6564
+ ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShareByCommits);
6427
6565
  busFactorRisk = toUnitInterval(
6428
- 1 - normalizeWithScale(evolutionMetrics.busFactor, evolutionScales.busFactor)
6566
+ 1 - normalizeWithScale(evolutionMetrics.busFactorByCommits, evolutionScales.busFactor)
6429
6567
  );
6430
6568
  const evolutionWeights = config.evolutionFactorWeights;
6431
6569
  evolutionFactor = toUnitInterval(
@@ -6482,8 +6620,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
6482
6620
  commitCount: evolutionMetrics?.commitCount ?? null,
6483
6621
  churnTotal: evolutionMetrics?.churnTotal ?? null,
6484
6622
  recentVolatility: evolutionMetrics?.recentVolatility ?? null,
6485
- topAuthorShare: evolutionMetrics?.topAuthorShare ?? null,
6486
- busFactor: evolutionMetrics?.busFactor ?? null,
6623
+ topAuthorShareByCommits: evolutionMetrics?.topAuthorShareByCommits ?? null,
6624
+ busFactorByCommits: evolutionMetrics?.busFactorByCommits ?? null,
6487
6625
  dependencyAffinity: round46(dependencyAffinity),
6488
6626
  repositoryExternalPressure: round46(dependencyComputation.repositoryExternalPressure),
6489
6627
  structuralAttenuation: round46(structuralAttenuation)
@@ -6551,8 +6689,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
6551
6689
  commitCount: context.rawMetrics.commitCount,
6552
6690
  churnTotal: context.rawMetrics.churnTotal,
6553
6691
  recentVolatility: context.rawMetrics.recentVolatility,
6554
- topAuthorShare: context.rawMetrics.topAuthorShare,
6555
- busFactor: context.rawMetrics.busFactor
6692
+ topAuthorShareByCommits: context.rawMetrics.topAuthorShareByCommits,
6693
+ busFactorByCommits: context.rawMetrics.busFactorByCommits
6556
6694
  },
6557
6695
  normalizedMetrics: {
6558
6696
  frequencyRisk: context.normalizedMetrics.frequencyRisk,