@getcodesentinel/codesentinel 1.15.0 → 1.17.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
@@ -1656,7 +1656,7 @@ var createReport = (snapshot, diff) => {
1656
1656
  confidence: repositoryConfidence(snapshot),
1657
1657
  dimensionScores: repositoryDimensionScores(snapshot)
1658
1658
  },
1659
- quality: snapshot.analysis.quality,
1659
+ health: snapshot.analysis.health,
1660
1660
  hotspots: hotspotItems(snapshot),
1661
1661
  structural: {
1662
1662
  cycleCount: snapshot.analysis.structural.metrics.cycleCount,
@@ -1728,19 +1728,21 @@ var renderTextReport = (report) => {
1728
1728
  lines.push(` external: ${report.repository.dimensionScores.external ?? "n/a"}`);
1729
1729
  lines.push(` interactions: ${report.repository.dimensionScores.interactions ?? "n/a"}`);
1730
1730
  lines.push("");
1731
- lines.push("Quality Summary");
1732
- lines.push(` qualityScore: ${report.quality.qualityScore}`);
1733
- lines.push(` normalizedScore: ${report.quality.normalizedScore}`);
1734
- lines.push(` modularity: ${report.quality.dimensions.modularity}`);
1735
- lines.push(` changeHygiene: ${report.quality.dimensions.changeHygiene}`);
1736
- lines.push(` testHealth: ${report.quality.dimensions.testHealth}`);
1731
+ lines.push("Health Summary");
1732
+ lines.push(` healthScore: ${report.health.healthScore}`);
1733
+ lines.push(` normalizedScore: ${report.health.normalizedScore}`);
1734
+ lines.push(` modularity: ${report.health.dimensions.modularity}`);
1735
+ lines.push(` changeHygiene: ${report.health.dimensions.changeHygiene}`);
1736
+ lines.push(` testHealth: ${report.health.dimensions.testHealth}`);
1737
+ lines.push(` ownershipDistribution: ${report.health.dimensions.ownershipDistribution}`);
1737
1738
  lines.push(" topIssues:");
1738
- for (const issue of report.quality.topIssues.slice(0, 5)) {
1739
+ for (const issue of report.health.topIssues.slice(0, 5)) {
1740
+ const ruleSuffix = issue.ruleId === void 0 ? "" : ` [rule=${issue.ruleId}]`;
1739
1741
  lines.push(
1740
- ` - [${issue.severity}] (${issue.dimension}) ${issue.id} @ ${issue.target}: ${issue.message}`
1742
+ ` - [${issue.severity}] (${issue.dimension}) ${issue.id}${ruleSuffix} @ ${issue.target}: ${issue.message}`
1741
1743
  );
1742
1744
  }
1743
- if (report.quality.topIssues.length === 0) {
1745
+ if (report.health.topIssues.length === 0) {
1744
1746
  lines.push(" - none");
1745
1747
  }
1746
1748
  lines.push("");
@@ -1819,19 +1821,21 @@ var renderMarkdownReport = (report) => {
1819
1821
  lines.push(`- external: \`${report.repository.dimensionScores.external ?? "n/a"}\``);
1820
1822
  lines.push(`- interactions: \`${report.repository.dimensionScores.interactions ?? "n/a"}\``);
1821
1823
  lines.push("");
1822
- lines.push("## Quality Summary");
1823
- lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
1824
- lines.push(`- normalizedScore: \`${report.quality.normalizedScore}\``);
1825
- lines.push(`- modularity: \`${report.quality.dimensions.modularity}\``);
1826
- lines.push(`- changeHygiene: \`${report.quality.dimensions.changeHygiene}\``);
1827
- lines.push(`- testHealth: \`${report.quality.dimensions.testHealth}\``);
1828
- if (report.quality.topIssues.length === 0) {
1824
+ lines.push("## Health Summary");
1825
+ lines.push(`- healthScore: \`${report.health.healthScore}\``);
1826
+ lines.push(`- normalizedScore: \`${report.health.normalizedScore}\``);
1827
+ lines.push(`- modularity: \`${report.health.dimensions.modularity}\``);
1828
+ lines.push(`- changeHygiene: \`${report.health.dimensions.changeHygiene}\``);
1829
+ lines.push(`- testHealth: \`${report.health.dimensions.testHealth}\``);
1830
+ lines.push(`- ownershipDistribution: \`${report.health.dimensions.ownershipDistribution}\``);
1831
+ if (report.health.topIssues.length === 0) {
1829
1832
  lines.push("- top issues: none");
1830
1833
  } else {
1831
1834
  lines.push("- top issues:");
1832
- for (const issue of report.quality.topIssues.slice(0, 5)) {
1835
+ for (const issue of report.health.topIssues.slice(0, 5)) {
1836
+ const ruleSuffix = issue.ruleId === void 0 ? "" : ` [rule=${issue.ruleId}]`;
1833
1837
  lines.push(
1834
- ` - [${issue.severity}] \`${issue.id}\` (\`${issue.dimension}\`) @ \`${issue.target}\`: ${issue.message}`
1838
+ ` - [${issue.severity}] \`${issue.id}\`${ruleSuffix} (\`${issue.dimension}\`) @ \`${issue.target}\`: ${issue.message}`
1835
1839
  );
1836
1840
  }
1837
1841
  }
@@ -1981,14 +1985,20 @@ var requireDiff = (input, gateId) => {
1981
1985
  };
1982
1986
  var validateGateConfig = (input) => {
1983
1987
  const config = input.gateConfig;
1984
- if (config.maxRepoDelta !== void 0 && (!Number.isFinite(config.maxRepoDelta) || config.maxRepoDelta < 0)) {
1985
- throw new GovernanceConfigurationError("max-repo-delta must be a finite number >= 0");
1988
+ if (config.maxRiskDelta !== void 0 && (!Number.isFinite(config.maxRiskDelta) || config.maxRiskDelta < 0)) {
1989
+ throw new GovernanceConfigurationError("max-risk-delta must be a finite number >= 0");
1990
+ }
1991
+ if (config.maxHealthDelta !== void 0 && (!Number.isFinite(config.maxHealthDelta) || config.maxHealthDelta < 0)) {
1992
+ throw new GovernanceConfigurationError("max-health-delta must be a finite number >= 0");
1986
1993
  }
1987
1994
  if (config.maxNewHotspots !== void 0 && (!Number.isInteger(config.maxNewHotspots) || config.maxNewHotspots < 0)) {
1988
1995
  throw new GovernanceConfigurationError("max-new-hotspots must be an integer >= 0");
1989
1996
  }
1990
- if (config.maxRepoScore !== void 0 && (!Number.isFinite(config.maxRepoScore) || config.maxRepoScore < 0 || config.maxRepoScore > 100)) {
1991
- throw new GovernanceConfigurationError("max-repo-score must be a number in [0, 100]");
1997
+ if (config.maxRiskScore !== void 0 && (!Number.isFinite(config.maxRiskScore) || config.maxRiskScore < 0 || config.maxRiskScore > 100)) {
1998
+ throw new GovernanceConfigurationError("max-risk-score must be a number in [0, 100]");
1999
+ }
2000
+ if (config.minHealthScore !== void 0 && (!Number.isFinite(config.minHealthScore) || config.minHealthScore < 0 || config.minHealthScore > 100)) {
2001
+ throw new GovernanceConfigurationError("min-health-score must be a number in [0, 100]");
1992
2002
  }
1993
2003
  if (config.newHotspotScoreThreshold !== void 0 && (!Number.isFinite(config.newHotspotScoreThreshold) || config.newHotspotScoreThreshold < 0 || config.newHotspotScoreThreshold > 100)) {
1994
2004
  throw new GovernanceConfigurationError(
@@ -2001,41 +2011,76 @@ var evaluateGates = (input) => {
2001
2011
  const config = input.gateConfig;
2002
2012
  const violations = [];
2003
2013
  const evaluatedGates = [];
2004
- if (config.maxRepoScore !== void 0) {
2005
- evaluatedGates.push("max-repo-score");
2014
+ if (config.maxRiskScore !== void 0) {
2015
+ evaluatedGates.push("max-risk-score");
2006
2016
  const current = input.current.analysis.risk.riskScore;
2007
- if (current > config.maxRepoScore) {
2017
+ if (current > config.maxRiskScore) {
2008
2018
  violations.push(
2009
2019
  makeViolation(
2010
- "max-repo-score",
2020
+ "max-risk-score",
2011
2021
  "error",
2012
- `Repository score ${current} exceeds configured max ${config.maxRepoScore}.`,
2022
+ `Risk score ${current} exceeds configured max ${config.maxRiskScore}.`,
2013
2023
  [input.current.analysis.structural.targetPath],
2014
2024
  [{ kind: "repository_metric", metric: "riskScore" }]
2015
2025
  )
2016
2026
  );
2017
2027
  }
2018
2028
  }
2019
- if (config.maxRepoDelta !== void 0) {
2020
- evaluatedGates.push("max-repo-delta");
2021
- requireDiff(input, "max-repo-delta");
2029
+ if (config.minHealthScore !== void 0) {
2030
+ evaluatedGates.push("min-health-score");
2031
+ const current = input.current.analysis.health.healthScore;
2032
+ if (current < config.minHealthScore) {
2033
+ violations.push(
2034
+ makeViolation(
2035
+ "min-health-score",
2036
+ "error",
2037
+ `Health score ${current} is below configured minimum ${config.minHealthScore}.`,
2038
+ [input.current.analysis.structural.targetPath],
2039
+ [{ kind: "repository_metric", metric: "healthScore" }]
2040
+ )
2041
+ );
2042
+ }
2043
+ }
2044
+ if (config.maxRiskDelta !== void 0) {
2045
+ evaluatedGates.push("max-risk-delta");
2046
+ requireDiff(input, "max-risk-delta");
2022
2047
  const baseline = input.baseline;
2023
2048
  if (baseline === void 0) {
2024
- throw new GovernanceConfigurationError("max-repo-delta requires baseline snapshot");
2049
+ throw new GovernanceConfigurationError("max-risk-delta requires baseline snapshot");
2025
2050
  }
2026
2051
  const delta = input.current.analysis.risk.normalizedScore - baseline.analysis.risk.normalizedScore;
2027
- if (delta > config.maxRepoDelta) {
2052
+ if (delta > config.maxRiskDelta) {
2028
2053
  violations.push(
2029
2054
  makeViolation(
2030
- "max-repo-delta",
2055
+ "max-risk-delta",
2031
2056
  "error",
2032
- `Repository normalized score delta ${delta.toFixed(4)} exceeds allowed ${config.maxRepoDelta}.`,
2057
+ `Risk normalized score delta ${delta.toFixed(4)} exceeds allowed ${config.maxRiskDelta}.`,
2033
2058
  [input.current.analysis.structural.targetPath],
2034
2059
  [{ kind: "repository_metric", metric: "normalizedScore" }]
2035
2060
  )
2036
2061
  );
2037
2062
  }
2038
2063
  }
2064
+ if (config.maxHealthDelta !== void 0) {
2065
+ evaluatedGates.push("max-health-delta");
2066
+ requireDiff(input, "max-health-delta");
2067
+ const baseline = input.baseline;
2068
+ if (baseline === void 0) {
2069
+ throw new GovernanceConfigurationError("max-health-delta requires baseline snapshot");
2070
+ }
2071
+ const delta = input.current.analysis.health.normalizedScore - baseline.analysis.health.normalizedScore;
2072
+ if (delta < -config.maxHealthDelta) {
2073
+ violations.push(
2074
+ makeViolation(
2075
+ "max-health-delta",
2076
+ "error",
2077
+ `Health normalized score delta ${delta.toFixed(4)} is below allowed minimum ${(-config.maxHealthDelta).toFixed(4)}.`,
2078
+ [input.current.analysis.structural.targetPath],
2079
+ [{ kind: "repository_metric", metric: "healthNormalizedScore" }]
2080
+ )
2081
+ );
2082
+ }
2083
+ }
2039
2084
  if (config.noNewCycles === true) {
2040
2085
  evaluatedGates.push("no-new-cycles");
2041
2086
  requireDiff(input, "no-new-cycles");
@@ -2493,7 +2538,7 @@ var resolveAutoBaselineRef = async (input) => {
2493
2538
 
2494
2539
  // src/index.ts
2495
2540
  import { readFileSync as readFileSync2 } from "fs";
2496
- import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
2541
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
2497
2542
  import { dirname as dirname2, resolve as resolve5 } from "path";
2498
2543
  import { fileURLToPath } from "url";
2499
2544
 
@@ -2535,11 +2580,11 @@ var createSummaryShape = (summary) => ({
2535
2580
  fragileClusterCount: summary.risk.fragileClusters.length,
2536
2581
  dependencyAmplificationZoneCount: summary.risk.dependencyAmplificationZones.length
2537
2582
  },
2538
- quality: {
2539
- qualityScore: summary.quality.qualityScore,
2540
- normalizedScore: summary.quality.normalizedScore,
2541
- dimensions: summary.quality.dimensions,
2542
- topIssues: summary.quality.topIssues.slice(0, 5)
2583
+ health: {
2584
+ healthScore: summary.health.healthScore,
2585
+ normalizedScore: summary.health.normalizedScore,
2586
+ dimensions: summary.health.dimensions,
2587
+ topIssues: summary.health.topIssues.slice(0, 5)
2543
2588
  }
2544
2589
  });
2545
2590
  var formatAnalyzeOutput = (summary, mode) => mode === "json" ? JSON.stringify(summary, null, 2) : JSON.stringify(createSummaryShape(summary), null, 2);
@@ -2890,7 +2935,7 @@ import { mkdir, readFile, writeFile } from "fs/promises";
2890
2935
  import { homedir } from "os";
2891
2936
  import { dirname, join as join3 } from "path";
2892
2937
  import { stderr, stdin } from "process";
2893
- import { clearScreenDown, cursorTo, emitKeypressEvents, moveCursor } from "readline";
2938
+ import { clearScreenDown, cursorTo, emitKeypressEvents } from "readline";
2894
2939
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2895
2940
  var UPDATE_CACHE_PATH = join3(homedir(), ".cache", "codesentinel", "update-check.json");
2896
2941
  var SEMVER_PATTERN = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
@@ -3082,11 +3127,15 @@ var fetchLatestVersion = async (packageName) => {
3082
3127
  }
3083
3128
  return parseNpmViewVersionOutput(result.stdout);
3084
3129
  };
3085
- var renderUpdatePrompt = (latestVersion, currentVersion, selectedIndex) => {
3086
- const options = ["Install update now", "Not now (continue current command)"];
3130
+ var renderUpdatePrompt = (packageName, latestVersion, currentVersion, selectedIndex) => {
3131
+ const options = [
3132
+ `1. Update now (runs \`npm install -g ${packageName}\`)`,
3133
+ "2. Skip"
3134
+ ];
3087
3135
  const lines = [
3088
- `${ANSI.cyan}${ANSI.bold}CodeSentinel Update Available${ANSI.reset}`,
3089
- `${ANSI.dim}Current: ${currentVersion} Latest: ${latestVersion}${ANSI.reset}`,
3136
+ ` ${ANSI.bold}${ANSI.cyan}\u2728 Update available! ${currentVersion} -> ${latestVersion}${ANSI.reset}`,
3137
+ "",
3138
+ ` ${ANSI.dim}Release notes: https://github.com/getcodesentinel/codesentinel/releases/latest${ANSI.reset}`,
3090
3139
  "",
3091
3140
  ...options.map((option, index) => {
3092
3141
  const selected = index === selectedIndex;
@@ -3095,12 +3144,12 @@ var renderUpdatePrompt = (latestVersion, currentVersion, selectedIndex) => {
3095
3144
  return `${prefix} ${text}`;
3096
3145
  }),
3097
3146
  "",
3098
- `${ANSI.dim}Use \u2191/\u2193 to choose, Enter to confirm.${ANSI.reset}`
3147
+ ` ${ANSI.dim}Use \u2191/\u2193 to choose. Press enter to continue${ANSI.reset}`
3099
3148
  ];
3100
3149
  stderr.write(lines.join("\n"));
3101
3150
  return lines.length;
3102
3151
  };
3103
- var promptInstall = async (latestVersion, currentVersion) => {
3152
+ var promptInstall = async (packageName, latestVersion, currentVersion) => {
3104
3153
  if (!stdin.isTTY || !stderr.isTTY || typeof stdin.setRawMode !== "function") {
3105
3154
  stderr.write(
3106
3155
  `New version ${latestVersion} is available (current ${currentVersion}). Run: npm install -g @getcodesentinel/codesentinel@latest
@@ -3111,18 +3160,14 @@ var promptInstall = async (latestVersion, currentVersion) => {
3111
3160
  return await new Promise((resolve6) => {
3112
3161
  emitKeypressEvents(stdin);
3113
3162
  let selectedIndex = 0;
3114
- let renderedLines = 0;
3115
3163
  const previousRawMode = stdin.isRaw;
3116
3164
  const clearPromptArea = () => {
3117
- if (renderedLines > 0) {
3118
- moveCursor(stderr, 0, -(renderedLines - 1));
3119
- }
3120
- cursorTo(stderr, 0);
3165
+ cursorTo(stderr, 0, 0);
3121
3166
  clearScreenDown(stderr);
3122
3167
  };
3123
3168
  const redraw = () => {
3124
3169
  clearPromptArea();
3125
- renderedLines = renderUpdatePrompt(latestVersion, currentVersion, selectedIndex);
3170
+ renderUpdatePrompt(packageName, latestVersion, currentVersion, selectedIndex);
3126
3171
  };
3127
3172
  const cleanup = (choice) => {
3128
3173
  stdin.off("keypress", onKeypress);
@@ -3134,7 +3179,7 @@ var promptInstall = async (latestVersion, currentVersion) => {
3134
3179
  if (choice === "install") {
3135
3180
  stderr.write(`${ANSI.yellow}Installing latest CodeSentinel...${ANSI.reset}
3136
3181
  `);
3137
- } else if (renderedLines > 0) {
3182
+ } else {
3138
3183
  stderr.write("\n");
3139
3184
  }
3140
3185
  resolve6(choice);
@@ -3193,7 +3238,7 @@ var checkForCliUpdates = async (input) => {
3193
3238
  if (comparison === null || comparison <= 0) {
3194
3239
  return;
3195
3240
  }
3196
- const choice = await promptInstall(latestVersion, input.currentVersion);
3241
+ const choice = await promptInstall(input.packageName, latestVersion, input.currentVersion);
3197
3242
  if (choice === "interrupt") {
3198
3243
  process.exit(130);
3199
3244
  }
@@ -3216,8 +3261,7 @@ var checkForCliUpdates = async (input) => {
3216
3261
  };
3217
3262
 
3218
3263
  // src/application/run-analyze-command.ts
3219
- import { readFile as readFile2 } from "fs/promises";
3220
- import { join as join4, resolve as resolve3 } from "path";
3264
+ import { resolve as resolve3 } from "path";
3221
3265
 
3222
3266
  // ../code-graph/dist/index.js
3223
3267
  import { extname, isAbsolute, relative, resolve as resolve2 } from "path";
@@ -4265,7 +4309,7 @@ var analyzeRepositoryEvolutionFromGit = (input, onProgress) => {
4265
4309
  return analyzeRepositoryEvolution(input, historyProvider, onProgress);
4266
4310
  };
4267
4311
 
4268
- // ../quality-engine/dist/index.js
4312
+ // ../health-engine/dist/index.js
4269
4313
  var clamp01 = (value) => {
4270
4314
  if (!Number.isFinite(value)) {
4271
4315
  return 0;
@@ -4305,16 +4349,66 @@ var concentration = (rawValues) => {
4305
4349
  return clamp01(normalized);
4306
4350
  };
4307
4351
  var DIMENSION_WEIGHTS = {
4308
- modularity: 0.45,
4309
- changeHygiene: 0.35,
4310
- testHealth: 0.2
4352
+ modularity: 0.35,
4353
+ changeHygiene: 0.3,
4354
+ testHealth: 0.2,
4355
+ ownershipDistribution: 0.15
4356
+ };
4357
+ var HEALTH_TRACE_VERSION = "1";
4358
+ var toPercentage = (normalizedHealth) => round45(clamp01(normalizedHealth) * 100);
4359
+ var dampenForSmallSamples = (penalty, sampleSize, warmupSize, minimumWeight = 0.35) => {
4360
+ const reliability = clamp01(sampleSize / Math.max(1, warmupSize));
4361
+ const dampeningWeight = minimumWeight + (1 - minimumWeight) * reliability;
4362
+ return clamp01(penalty) * dampeningWeight;
4363
+ };
4364
+ var topPercentShare = (values, fraction) => {
4365
+ const positive = values.filter((value) => value > 0).sort((a, b) => b - a);
4366
+ if (positive.length === 0) {
4367
+ return 0;
4368
+ }
4369
+ const topCount = Math.max(1, Math.ceil(positive.length * clamp01(fraction)));
4370
+ const total = positive.reduce((sum, value) => sum + value, 0);
4371
+ const topTotal = positive.slice(0, topCount).reduce((sum, value) => sum + value, 0);
4372
+ if (total <= 0) {
4373
+ return 0;
4374
+ }
4375
+ return clamp01(topTotal / total);
4311
4376
  };
4312
- var TODO_FIXME_MAX_IMPACT = 0.08;
4313
- var toPercentage = (normalizedQuality) => round45(clamp01(normalizedQuality) * 100);
4377
+ var normalizedEntropy = (weights) => {
4378
+ const positive = weights.filter((value) => value > 0);
4379
+ if (positive.length <= 1) {
4380
+ return 0;
4381
+ }
4382
+ const total = positive.reduce((sum, value) => sum + value, 0);
4383
+ if (total <= 0) {
4384
+ return 0;
4385
+ }
4386
+ const entropy = positive.reduce((sum, value) => {
4387
+ const p = value / total;
4388
+ return sum - p * Math.log(p);
4389
+ }, 0);
4390
+ return clamp01(entropy / Math.log(positive.length));
4391
+ };
4392
+ var toFactorTrace = (spec) => ({
4393
+ factorId: spec.factorId,
4394
+ contribution: round45(spec.penalty * spec.weight * 100),
4395
+ penalty: round45(spec.penalty),
4396
+ rawMetrics: spec.rawMetrics,
4397
+ normalizedMetrics: spec.normalizedMetrics,
4398
+ weight: round45(spec.weight),
4399
+ evidence: spec.evidence
4400
+ });
4401
+ var createDimensionTrace = (dimension, health, factors) => ({
4402
+ dimension,
4403
+ normalizedScore: round45(clamp01(health)),
4404
+ score: toPercentage(health),
4405
+ factors: factors.map((factor) => toFactorTrace(factor))
4406
+ });
4314
4407
  var filePaths = (structural) => structural.files.map((file) => file.relativePath);
4408
+ var normalizePath2 = (value) => value.replaceAll("\\", "/").toLowerCase();
4315
4409
  var isTestPath = (path) => {
4316
- const normalized = path.toLowerCase();
4317
- return normalized.includes("/__tests__/") || normalized.includes("\\__tests__\\") || normalized.includes(".test.") || normalized.includes(".spec.");
4410
+ const normalized = normalizePath2(path);
4411
+ return normalized.includes("/__tests__/") || normalized.includes("/tests/") || normalized.includes("/test/") || normalized.includes(".test.") || normalized.includes(".spec.");
4318
4412
  };
4319
4413
  var isSourcePath = (path) => {
4320
4414
  if (path.endsWith(".d.ts")) {
@@ -4322,148 +4416,721 @@ var isSourcePath = (path) => {
4322
4416
  }
4323
4417
  return !isTestPath(path);
4324
4418
  };
4419
+ var hasTestDirectory = (paths) => paths.some((path) => {
4420
+ const normalized = normalizePath2(path);
4421
+ return normalized.includes("/__tests__/") || normalized.includes("/tests/") || normalized.includes("/test/");
4422
+ });
4423
+ var moduleNameFromPath = (path) => {
4424
+ const normalized = path.replaceAll("\\", "/");
4425
+ const firstSegment = normalized.split("/")[0] ?? normalized;
4426
+ return firstSegment.length === 0 ? normalized : firstSegment;
4427
+ };
4325
4428
  var pushIssue = (issues, issue) => {
4326
4429
  issues.push({
4327
4430
  ...issue,
4328
4431
  severity: issue.severity ?? "warn"
4329
4432
  });
4330
4433
  };
4331
- var computeRepositoryQualitySummary = (input) => {
4434
+ var weightedPenalty = (factors) => clamp01(factors.reduce((sum, factor) => sum + factor.penalty * factor.weight, 0));
4435
+ var computeRepositoryHealthSummary = (input) => {
4436
+ const ownershipPenaltyMultiplier = clamp01(input.config?.ownershipPenaltyMultiplier ?? 1);
4332
4437
  const issues = [];
4333
4438
  const sourceFileSet = new Set(input.structural.files.map((file) => file.relativePath));
4334
- const cycleCount = input.structural.metrics.cycleCount;
4335
- const cycleSizeAverage = input.structural.cycles.length === 0 ? 0 : average(input.structural.cycles.map((cycle) => cycle.nodes.length));
4336
- const cyclePenalty = clamp01(cycleCount / 6) * 0.7 + clamp01((cycleSizeAverage - 2) / 8) * 0.3;
4337
- if (cycleCount > 0) {
4439
+ const sourceFileCount = Math.max(1, input.structural.files.length);
4440
+ const structuralEdges = input.structural.edges;
4441
+ const cycleSets = input.structural.cycles.map((cycle) => new Set(cycle.nodes)).filter((set) => set.size >= 2);
4442
+ const cycleNodeSet = /* @__PURE__ */ new Set();
4443
+ for (const cycleSet of cycleSets) {
4444
+ for (const node of cycleSet) {
4445
+ cycleNodeSet.add(node);
4446
+ }
4447
+ }
4448
+ const edgesInsideCycles = structuralEdges.filter(
4449
+ (edge) => cycleSets.some((cycleSet) => cycleSet.has(edge.from) && cycleSet.has(edge.to))
4450
+ ).length;
4451
+ const cycleEdgeRatio = structuralEdges.length === 0 ? 0 : clamp01(edgesInsideCycles / structuralEdges.length);
4452
+ const cycleNodeRatio = clamp01(cycleNodeSet.size / sourceFileCount);
4453
+ const cycleDensityPenalty = clamp01(
4454
+ clamp01(cycleEdgeRatio / 0.2) * 0.75 + clamp01(cycleNodeRatio / 0.35) * 0.25
4455
+ );
4456
+ const fanInConcentration = concentration(input.structural.files.map((file) => file.fanIn));
4457
+ const fanOutConcentration = concentration(input.structural.files.map((file) => file.fanOut));
4458
+ const fanConcentration = average([fanInConcentration, fanOutConcentration]);
4459
+ const centralityPressure = input.structural.files.map((file) => file.fanIn + file.fanOut);
4460
+ const centralityConcentration = concentration(centralityPressure);
4461
+ let structuralHotspotOverlap = 0;
4462
+ if (input.evolution.available) {
4463
+ const evolutionSourceFiles = input.evolution.files.filter(
4464
+ (file) => sourceFileSet.has(file.filePath)
4465
+ );
4466
+ const topStructuralCount = Math.max(1, Math.ceil(sourceFileCount * 0.1));
4467
+ const topChangeCount = Math.max(1, Math.ceil(Math.max(1, evolutionSourceFiles.length) * 0.1));
4468
+ const topStructural = new Set(
4469
+ [...input.structural.files].map((file) => ({
4470
+ filePath: file.relativePath,
4471
+ pressure: file.fanIn + file.fanOut
4472
+ })).sort((a, b) => b.pressure - a.pressure || a.filePath.localeCompare(b.filePath)).slice(0, topStructuralCount).map((item) => item.filePath)
4473
+ );
4474
+ const topChange = new Set(
4475
+ [...evolutionSourceFiles].sort((a, b) => b.churnTotal - a.churnTotal || a.filePath.localeCompare(b.filePath)).slice(0, topChangeCount).map((item) => item.filePath)
4476
+ );
4477
+ const overlapCount = [...topStructural].filter((filePath) => topChange.has(filePath)).length;
4478
+ structuralHotspotOverlap = topStructural.size === 0 || topChange.size === 0 ? 0 : clamp01(overlapCount / Math.min(topStructural.size, topChange.size));
4479
+ }
4480
+ const modularityFactors = [
4481
+ {
4482
+ factorId: "health.modularity.cycle_density",
4483
+ penalty: cycleDensityPenalty,
4484
+ rawMetrics: {
4485
+ cycleCount: input.structural.metrics.cycleCount,
4486
+ cycleEdgeRatio: round45(cycleEdgeRatio),
4487
+ cycleNodeRatio: round45(cycleNodeRatio)
4488
+ },
4489
+ normalizedMetrics: {
4490
+ cycleDensityPenalty: round45(cycleDensityPenalty)
4491
+ },
4492
+ weight: 0.4,
4493
+ evidence: [{ kind: "repository_metric", metric: "structural.cycleEdgeRatio" }]
4494
+ },
4495
+ {
4496
+ factorId: "health.modularity.fan_concentration",
4497
+ penalty: fanConcentration,
4498
+ rawMetrics: {
4499
+ fanInConcentration: round45(fanInConcentration),
4500
+ fanOutConcentration: round45(fanOutConcentration)
4501
+ },
4502
+ normalizedMetrics: {
4503
+ fanConcentration: round45(fanConcentration)
4504
+ },
4505
+ weight: 0.25,
4506
+ evidence: [{ kind: "repository_metric", metric: "structural.files.fanIn/fanOut" }]
4507
+ },
4508
+ {
4509
+ factorId: "health.modularity.centrality_concentration",
4510
+ penalty: centralityConcentration,
4511
+ rawMetrics: {
4512
+ centralityConcentration: round45(centralityConcentration)
4513
+ },
4514
+ normalizedMetrics: {
4515
+ centralityConcentration: round45(centralityConcentration)
4516
+ },
4517
+ weight: 0.2,
4518
+ evidence: [{ kind: "repository_metric", metric: "structural.centralityPressure" }]
4519
+ },
4520
+ {
4521
+ factorId: "health.modularity.hotspot_overlap",
4522
+ penalty: structuralHotspotOverlap,
4523
+ rawMetrics: {
4524
+ structuralHotspotOverlap: round45(structuralHotspotOverlap)
4525
+ },
4526
+ normalizedMetrics: {
4527
+ structuralHotspotOverlap: round45(structuralHotspotOverlap)
4528
+ },
4529
+ weight: 0.15,
4530
+ evidence: [{ kind: "repository_metric", metric: "structural.evolution.hotspotOverlap" }]
4531
+ }
4532
+ ];
4533
+ const modularityPenalty = dampenForSmallSamples(
4534
+ weightedPenalty(modularityFactors),
4535
+ sourceFileCount,
4536
+ 8,
4537
+ 0.45
4538
+ );
4539
+ if (cycleDensityPenalty >= 0.35) {
4540
+ const firstCycle = input.structural.cycles[0];
4338
4541
  pushIssue(issues, {
4339
- id: "quality.modularity.structural_cycles",
4542
+ id: "health.modularity.cycle_density",
4543
+ ruleId: "graph.cycle_density",
4544
+ signal: "structural.cycleEdgeRatio",
4340
4545
  dimension: "modularity",
4341
- target: input.structural.cycles[0]?.nodes.slice().sort((a, b) => a.localeCompare(b)).join(" -> ") ?? input.structural.targetPath,
4342
- message: `${cycleCount} structural cycle(s) increase coupling and refactor cost.`,
4343
- severity: cycleCount >= 3 ? "error" : "warn",
4344
- impact: round45(cyclePenalty * 0.6)
4546
+ target: firstCycle?.nodes.slice().sort((a, b) => a.localeCompare(b)).join(" -> ") ?? input.structural.targetPath,
4547
+ message: "Dependencies inside cycles consume a high share of graph edges, reducing refactor flexibility.",
4548
+ severity: cycleDensityPenalty >= 0.7 ? "error" : "warn",
4549
+ evidenceMetrics: {
4550
+ cycleCount: input.structural.metrics.cycleCount,
4551
+ cycleEdgeRatio: round45(cycleEdgeRatio),
4552
+ cycleNodeRatio: round45(cycleNodeRatio)
4553
+ },
4554
+ impact: round45(modularityPenalty * 0.35)
4345
4555
  });
4346
4556
  }
4347
- const fanInConcentration = concentration(input.structural.files.map((file) => file.fanIn));
4348
- const fanOutConcentration = concentration(input.structural.files.map((file) => file.fanOut));
4349
- const centralityConcentration = average([fanInConcentration, fanOutConcentration]);
4350
4557
  if (centralityConcentration >= 0.5) {
4351
4558
  const hottest = [...input.structural.files].map((file) => ({
4352
4559
  path: file.relativePath,
4353
4560
  pressure: file.fanIn + file.fanOut
4354
4561
  })).sort((a, b) => b.pressure - a.pressure || a.path.localeCompare(b.path))[0];
4355
4562
  pushIssue(issues, {
4356
- id: "quality.modularity.centrality_concentration",
4563
+ id: "health.modularity.centrality_concentration",
4564
+ ruleId: "graph.centrality_concentration",
4565
+ signal: "structural.centralityPressure",
4357
4566
  dimension: "modularity",
4358
4567
  target: hottest?.path ?? input.structural.targetPath,
4359
- message: "Fan-in/fan-out pressure is concentrated in a small set of files.",
4360
- impact: round45(centralityConcentration * 0.5)
4568
+ message: "Dependency flow is concentrated in a narrow set of files, creating architectural bottlenecks.",
4569
+ evidenceMetrics: {
4570
+ fanConcentration: round45(fanConcentration),
4571
+ centralityConcentration: round45(centralityConcentration)
4572
+ },
4573
+ impact: round45(modularityPenalty * 0.25)
4361
4574
  });
4362
4575
  }
4363
- let churnConcentration = 0;
4364
- let volatilityConcentration = 0;
4365
- let couplingDensity = 0;
4366
- let couplingIntensity = 0;
4576
+ if (structuralHotspotOverlap >= 0.5) {
4577
+ pushIssue(issues, {
4578
+ id: "health.modularity.hotspot_overlap",
4579
+ ruleId: "graph.hotspot_overlap",
4580
+ signal: "structural.evolution.hotspotOverlap",
4581
+ dimension: "modularity",
4582
+ target: input.structural.targetPath,
4583
+ message: "Structural hubs overlap with top churn hotspots, making change pressure harder to isolate.",
4584
+ evidenceMetrics: {
4585
+ structuralHotspotOverlap: round45(structuralHotspotOverlap)
4586
+ },
4587
+ impact: round45(modularityPenalty * 0.2)
4588
+ });
4589
+ }
4590
+ let churnConcentrationPenalty = 0;
4591
+ let volatilityConcentrationPenalty = 0;
4592
+ let coChangeClusterPenalty = 0;
4593
+ let top10PercentFilesChurnShare = 0;
4594
+ let top10PercentFilesVolatilityShare = 0;
4595
+ let denseCoChangePairRatio = 0;
4367
4596
  if (input.evolution.available) {
4368
4597
  const evolutionSourceFiles = input.evolution.files.filter(
4369
4598
  (file) => sourceFileSet.has(file.filePath)
4370
4599
  );
4371
- churnConcentration = concentration(evolutionSourceFiles.map((file) => file.churnTotal));
4372
- volatilityConcentration = concentration(
4373
- evolutionSourceFiles.map((file) => file.recentVolatility)
4600
+ const evolutionFileCount = evolutionSourceFiles.length;
4601
+ top10PercentFilesChurnShare = topPercentShare(
4602
+ evolutionSourceFiles.map((file) => file.churnTotal),
4603
+ 0.1
4604
+ );
4605
+ top10PercentFilesVolatilityShare = topPercentShare(
4606
+ evolutionSourceFiles.map((file) => file.recentVolatility),
4607
+ 0.1
4374
4608
  );
4375
- const fileCount = Math.max(1, evolutionSourceFiles.length);
4376
- const maxPairs = fileCount * (fileCount - 1) / 2;
4377
4609
  const sourcePairs = input.evolution.coupling.pairs.filter(
4378
4610
  (pair) => sourceFileSet.has(pair.fileA) && sourceFileSet.has(pair.fileB)
4379
4611
  );
4380
- couplingDensity = maxPairs <= 0 ? 0 : clamp01(sourcePairs.length / maxPairs);
4381
- couplingIntensity = average(sourcePairs.map((pair) => pair.couplingScore));
4382
- if (churnConcentration >= 0.45) {
4612
+ const maxPairs = evolutionFileCount * (evolutionFileCount - 1) / 2;
4613
+ const densePairs = sourcePairs.filter((pair) => pair.couplingScore >= 0.55);
4614
+ denseCoChangePairRatio = maxPairs <= 0 ? 0 : clamp01(densePairs.length / maxPairs);
4615
+ const couplingScoreConcentration = concentration(sourcePairs.map((pair) => pair.couplingScore));
4616
+ churnConcentrationPenalty = dampenForSmallSamples(
4617
+ clamp01((top10PercentFilesChurnShare - 0.35) / 0.55),
4618
+ evolutionFileCount,
4619
+ 12,
4620
+ 0.3
4621
+ );
4622
+ volatilityConcentrationPenalty = dampenForSmallSamples(
4623
+ clamp01((top10PercentFilesVolatilityShare - 0.35) / 0.55),
4624
+ evolutionFileCount,
4625
+ 12,
4626
+ 0.3
4627
+ );
4628
+ const coChangeRaw = average([
4629
+ clamp01(denseCoChangePairRatio / 0.2),
4630
+ couplingScoreConcentration
4631
+ ]);
4632
+ coChangeClusterPenalty = dampenForSmallSamples(coChangeRaw, sourcePairs.length, 20, 0.35);
4633
+ if (churnConcentrationPenalty >= 0.35) {
4383
4634
  const mostChurn = [...evolutionSourceFiles].sort(
4384
4635
  (a, b) => b.churnTotal - a.churnTotal || a.filePath.localeCompare(b.filePath)
4385
4636
  )[0];
4386
4637
  pushIssue(issues, {
4387
- id: "quality.change_hygiene.churn_concentration",
4638
+ id: "health.change_hygiene.churn_concentration",
4639
+ ruleId: "git.churn_distribution",
4640
+ signal: "evolution.top10PercentFilesChurnShare",
4388
4641
  dimension: "changeHygiene",
4389
4642
  target: mostChurn?.filePath ?? input.structural.targetPath,
4390
- message: "Churn is concentrated in a narrow part of the codebase.",
4391
- impact: round45(churnConcentration * 0.45)
4643
+ message: "A small slice of files carries most churn, reducing change predictability.",
4644
+ evidenceMetrics: {
4645
+ top10PercentFilesChurnShare: round45(top10PercentFilesChurnShare)
4646
+ },
4647
+ impact: round45(churnConcentrationPenalty * 0.4)
4392
4648
  });
4393
4649
  }
4394
- if (volatilityConcentration >= 0.45) {
4650
+ if (volatilityConcentrationPenalty >= 0.35) {
4395
4651
  const volatileFile = [...evolutionSourceFiles].sort(
4396
4652
  (a, b) => b.recentVolatility - a.recentVolatility || a.filePath.localeCompare(b.filePath)
4397
4653
  )[0];
4398
4654
  pushIssue(issues, {
4399
- id: "quality.change_hygiene.volatility_concentration",
4655
+ id: "health.change_hygiene.volatility_concentration",
4656
+ ruleId: "git.volatility_distribution",
4657
+ signal: "evolution.top10PercentFilesVolatilityShare",
4400
4658
  dimension: "changeHygiene",
4401
4659
  target: volatileFile?.filePath ?? input.structural.targetPath,
4402
- message: "Recent volatility is concentrated in files that change frequently.",
4403
- impact: round45(volatilityConcentration * 0.4)
4660
+ message: "Recent volatility is concentrated, increasing review and release uncertainty.",
4661
+ evidenceMetrics: {
4662
+ top10PercentFilesVolatilityShare: round45(top10PercentFilesVolatilityShare)
4663
+ },
4664
+ impact: round45(volatilityConcentrationPenalty * 0.3)
4404
4665
  });
4405
4666
  }
4406
- if (couplingDensity >= 0.35 || couplingIntensity >= 0.45) {
4667
+ if (coChangeClusterPenalty >= 0.35) {
4407
4668
  const strongestPair = [...sourcePairs].sort(
4408
4669
  (a, b) => b.couplingScore - a.couplingScore || `${a.fileA}|${a.fileB}`.localeCompare(`${b.fileA}|${b.fileB}`)
4409
4670
  )[0];
4410
4671
  pushIssue(issues, {
4411
- id: "quality.change_hygiene.coupling_density",
4672
+ id: "health.change_hygiene.dense_co_change_clusters",
4673
+ ruleId: "git.co_change_clusters",
4674
+ signal: "evolution.denseCoChangePairRatio",
4412
4675
  dimension: "changeHygiene",
4413
4676
  target: strongestPair === void 0 ? input.structural.targetPath : `${strongestPair.fileA}<->${strongestPair.fileB}`,
4414
- message: "Co-change relationships are dense, increasing coordination overhead.",
4415
- impact: round45(average([couplingDensity, couplingIntensity]) * 0.35)
4677
+ message: "Dense co-change clusters suggest wider coordination scope per change.",
4678
+ evidenceMetrics: {
4679
+ denseCoChangePairRatio: round45(denseCoChangePairRatio)
4680
+ },
4681
+ impact: round45(coChangeClusterPenalty * 0.3)
4416
4682
  });
4417
4683
  }
4418
4684
  }
4419
- const modularityPenalty = clamp01(cyclePenalty * 0.55 + centralityConcentration * 0.45);
4420
- const changeHygienePenalty = input.evolution.available ? clamp01(
4421
- churnConcentration * 0.4 + volatilityConcentration * 0.35 + couplingDensity * 0.15 + couplingIntensity * 0.1
4422
- ) : 0.25;
4685
+ const changeHygieneFactors = [
4686
+ {
4687
+ factorId: "health.change_hygiene.churn_concentration",
4688
+ penalty: churnConcentrationPenalty,
4689
+ rawMetrics: {
4690
+ top10PercentFilesChurnShare: round45(top10PercentFilesChurnShare)
4691
+ },
4692
+ normalizedMetrics: {
4693
+ churnConcentrationPenalty: round45(churnConcentrationPenalty)
4694
+ },
4695
+ weight: 0.4,
4696
+ evidence: [{ kind: "repository_metric", metric: "evolution.top10PercentFilesChurnShare" }]
4697
+ },
4698
+ {
4699
+ factorId: "health.change_hygiene.volatility_concentration",
4700
+ penalty: volatilityConcentrationPenalty,
4701
+ rawMetrics: {
4702
+ top10PercentFilesVolatilityShare: round45(top10PercentFilesVolatilityShare)
4703
+ },
4704
+ normalizedMetrics: {
4705
+ volatilityConcentrationPenalty: round45(volatilityConcentrationPenalty)
4706
+ },
4707
+ weight: 0.3,
4708
+ evidence: [
4709
+ {
4710
+ kind: "repository_metric",
4711
+ metric: "evolution.top10PercentFilesVolatilityShare"
4712
+ }
4713
+ ]
4714
+ },
4715
+ {
4716
+ factorId: "health.change_hygiene.dense_co_change_clusters",
4717
+ penalty: coChangeClusterPenalty,
4718
+ rawMetrics: {
4719
+ denseCoChangePairRatio: round45(denseCoChangePairRatio)
4720
+ },
4721
+ normalizedMetrics: {
4722
+ coChangeClusterPenalty: round45(coChangeClusterPenalty)
4723
+ },
4724
+ weight: 0.3,
4725
+ evidence: [{ kind: "repository_metric", metric: "evolution.denseCoChangePairRatio" }]
4726
+ }
4727
+ ];
4728
+ const changeHygienePenalty = input.evolution.available ? weightedPenalty(changeHygieneFactors) : 0.12;
4423
4729
  const paths = filePaths(input.structural);
4424
4730
  const testFiles = paths.filter((path) => isTestPath(path)).length;
4425
4731
  const sourceFiles = paths.filter((path) => isSourcePath(path)).length;
4426
4732
  const testRatio = sourceFiles <= 0 ? 1 : testFiles / sourceFiles;
4427
- const testPresencePenalty = sourceFiles <= 0 ? 0 : 1 - clamp01(testRatio / 0.3);
4428
- if (sourceFiles > 0 && testRatio < 0.2) {
4733
+ const testingDirectoryPresent = hasTestDirectory(paths);
4734
+ const testPresencePenalty = sourceFiles <= 0 ? 0 : testFiles === 0 ? 1 : 0;
4735
+ const testRatioPenalty = sourceFiles <= 0 ? 0 : 1 - clamp01(testRatio / 0.25);
4736
+ const testingDirectoryPenalty = sourceFiles <= 0 ? 0 : testingDirectoryPresent ? 0 : 0.35;
4737
+ const testHealthFactors = [
4738
+ {
4739
+ factorId: "health.test_health.test_file_presence",
4740
+ penalty: testPresencePenalty,
4741
+ rawMetrics: {
4742
+ sourceFiles,
4743
+ testFiles
4744
+ },
4745
+ normalizedMetrics: {
4746
+ testPresencePenalty: round45(testPresencePenalty)
4747
+ },
4748
+ weight: 0.4,
4749
+ evidence: [{ kind: "repository_metric", metric: "tests.filePresence" }]
4750
+ },
4751
+ {
4752
+ factorId: "health.test_health.test_to_source_ratio",
4753
+ penalty: testRatioPenalty,
4754
+ rawMetrics: {
4755
+ testToSourceRatio: round45(testRatio)
4756
+ },
4757
+ normalizedMetrics: {
4758
+ testRatioPenalty: round45(testRatioPenalty)
4759
+ },
4760
+ weight: 0.45,
4761
+ evidence: [{ kind: "repository_metric", metric: "tests.testToSourceRatio" }]
4762
+ },
4763
+ {
4764
+ factorId: "health.test_health.testing_directory_presence",
4765
+ penalty: testingDirectoryPenalty,
4766
+ rawMetrics: {
4767
+ testingDirectoryPresent: testingDirectoryPresent ? 1 : 0
4768
+ },
4769
+ normalizedMetrics: {
4770
+ testingDirectoryPenalty: round45(testingDirectoryPenalty)
4771
+ },
4772
+ weight: 0.15,
4773
+ evidence: [{ kind: "repository_metric", metric: "tests.directoryPresence" }]
4774
+ }
4775
+ ];
4776
+ const testHealthPenalty = dampenForSmallSamples(
4777
+ weightedPenalty(testHealthFactors),
4778
+ sourceFiles,
4779
+ 10,
4780
+ 0.3
4781
+ );
4782
+ if (sourceFiles > 0 && testFiles === 0) {
4429
4783
  pushIssue(issues, {
4430
- id: "quality.test_health.low_test_presence",
4784
+ id: "health.test_health.low_test_presence",
4785
+ ruleId: "tests.file_presence",
4786
+ signal: "tests.filePresence",
4431
4787
  dimension: "testHealth",
4432
4788
  target: input.structural.targetPath,
4433
- message: `Detected ${testFiles} test file(s) for ${sourceFiles} source file(s).`,
4434
- severity: testRatio === 0 ? "error" : "warn",
4435
- impact: round45(testPresencePenalty * 0.5)
4789
+ message: `No test files detected for ${sourceFiles} source file(s).`,
4790
+ severity: sourceFiles >= 12 ? "error" : "warn",
4791
+ evidenceMetrics: {
4792
+ sourceFiles,
4793
+ testFiles,
4794
+ testToSourceRatio: round45(testRatio)
4795
+ },
4796
+ impact: round45(testHealthPenalty * 0.45)
4436
4797
  });
4437
4798
  }
4438
- const todoFixmeCount = Math.max(0, input.todoFixmeCount ?? 0);
4439
- const todoFixmePenalty = clamp01(todoFixmeCount / 120) * TODO_FIXME_MAX_IMPACT;
4440
- if (todoFixmeCount > 0) {
4799
+ if (sourceFiles > 0 && testRatio < 0.12) {
4441
4800
  pushIssue(issues, {
4442
- id: "quality.change_hygiene.todo_fixme_load",
4443
- dimension: "changeHygiene",
4801
+ id: "health.test_health.low_test_ratio",
4802
+ ruleId: "tests.ratio",
4803
+ signal: "tests.testToSourceRatio",
4804
+ dimension: "testHealth",
4444
4805
  target: input.structural.targetPath,
4445
- message: `Found ${todoFixmeCount} TODO/FIXME marker(s); cleanup debt is accumulating.`,
4446
- impact: round45(todoFixmePenalty * 0.2)
4806
+ message: "Test-to-source ratio is low; long-term change confidence may degrade.",
4807
+ evidenceMetrics: {
4808
+ sourceFiles,
4809
+ testFiles,
4810
+ testToSourceRatio: round45(testRatio)
4811
+ },
4812
+ impact: round45(testHealthPenalty * 0.35)
4447
4813
  });
4448
4814
  }
4449
- const modularityQuality = clamp01(1 - modularityPenalty);
4450
- const changeHygieneQuality = clamp01(1 - clamp01(changeHygienePenalty + todoFixmePenalty));
4451
- const testHealthQuality = clamp01(1 - testPresencePenalty);
4815
+ if (input.evolution.available) {
4816
+ const evolutionSourceFiles = input.evolution.files.filter(
4817
+ (file) => sourceFileSet.has(file.filePath)
4818
+ );
4819
+ const authorTotals = /* @__PURE__ */ new Map();
4820
+ const moduleTotals = /* @__PURE__ */ new Map();
4821
+ const moduleAuthors = /* @__PURE__ */ new Map();
4822
+ let singleContributorFiles = 0;
4823
+ let trackedFiles = 0;
4824
+ for (const file of evolutionSourceFiles) {
4825
+ if (file.commitCount <= 0 || file.authorDistribution.length === 0) {
4826
+ continue;
4827
+ }
4828
+ trackedFiles += 1;
4829
+ const dominantShare = clamp01(file.authorDistribution[0]?.share ?? 0);
4830
+ if (file.authorDistribution.length === 1 || dominantShare >= 0.9) {
4831
+ singleContributorFiles += 1;
4832
+ }
4833
+ for (const author of file.authorDistribution) {
4834
+ const commits = Math.max(0, author.commits);
4835
+ if (commits <= 0) {
4836
+ continue;
4837
+ }
4838
+ const moduleName = moduleNameFromPath(file.filePath);
4839
+ const moduleAuthorTotals = moduleAuthors.get(moduleName) ?? /* @__PURE__ */ new Map();
4840
+ if (moduleAuthors.has(moduleName) === false) {
4841
+ moduleAuthors.set(moduleName, moduleAuthorTotals);
4842
+ }
4843
+ authorTotals.set(author.authorId, (authorTotals.get(author.authorId) ?? 0) + commits);
4844
+ moduleTotals.set(moduleName, (moduleTotals.get(moduleName) ?? 0) + commits);
4845
+ moduleAuthorTotals.set(
4846
+ author.authorId,
4847
+ (moduleAuthorTotals.get(author.authorId) ?? 0) + commits
4848
+ );
4849
+ }
4850
+ }
4851
+ const totalAuthorCommits = [...authorTotals.values()].reduce((sum, value) => sum + value, 0);
4852
+ const highestAuthorCommits = [...authorTotals.values()].sort((a, b) => b - a)[0] ?? 0;
4853
+ const topAuthorCommitShare = totalAuthorCommits <= 0 ? 0 : clamp01(highestAuthorCommits / totalAuthorCommits);
4854
+ const filesWithSingleContributorRatio = trackedFiles === 0 ? 0 : clamp01(singleContributorFiles / trackedFiles);
4855
+ const authorEntropy = normalizedEntropy([...authorTotals.values()]);
4856
+ let dominatedModules = 0;
4857
+ let trackedModules = 0;
4858
+ for (const [moduleName, moduleCommitTotal] of moduleTotals.entries()) {
4859
+ if (moduleCommitTotal < 5) {
4860
+ continue;
4861
+ }
4862
+ const perModuleAuthors = moduleAuthors.get(moduleName);
4863
+ if (perModuleAuthors === void 0) {
4864
+ continue;
4865
+ }
4866
+ trackedModules += 1;
4867
+ const topAuthorModuleCommits = [...perModuleAuthors.values()].sort((a, b) => b - a)[0] ?? 0;
4868
+ const moduleTopShare = moduleCommitTotal <= 0 ? 0 : topAuthorModuleCommits / moduleCommitTotal;
4869
+ if (moduleTopShare >= 0.8) {
4870
+ dominatedModules += 1;
4871
+ }
4872
+ }
4873
+ const modulesDominatedBySingleContributorRatio = trackedModules === 0 ? 0 : clamp01(dominatedModules / trackedModules);
4874
+ const ownershipSampleSize = trackedFiles;
4875
+ const ownershipCommitVolume = totalAuthorCommits;
4876
+ const ownershipReliability = average([
4877
+ clamp01(ownershipSampleSize / 12),
4878
+ clamp01(ownershipCommitVolume / 180)
4879
+ ]);
4880
+ const topAuthorPenalty = clamp01((topAuthorCommitShare - 0.55) / 0.4);
4881
+ const singleContributorPenalty = clamp01((filesWithSingleContributorRatio - 0.35) / 0.6);
4882
+ const entropyPenalty = clamp01((0.75 - authorEntropy) / 0.75);
4883
+ const moduleDominancePenalty = clamp01((modulesDominatedBySingleContributorRatio - 0.4) / 0.6);
4884
+ const ownershipBasePenalty = weightedPenalty([
4885
+ {
4886
+ factorId: "health.ownership.top_author_commit_share",
4887
+ penalty: topAuthorPenalty,
4888
+ rawMetrics: {
4889
+ topAuthorCommitShare: round45(topAuthorCommitShare)
4890
+ },
4891
+ normalizedMetrics: {
4892
+ topAuthorPenalty: round45(topAuthorPenalty)
4893
+ },
4894
+ weight: 0.35,
4895
+ evidence: [{ kind: "repository_metric", metric: "ownership.topAuthorCommitShare" }]
4896
+ },
4897
+ {
4898
+ factorId: "health.ownership.files_with_single_contributor_ratio",
4899
+ penalty: singleContributorPenalty,
4900
+ rawMetrics: {
4901
+ filesWithSingleContributorRatio: round45(filesWithSingleContributorRatio)
4902
+ },
4903
+ normalizedMetrics: {
4904
+ singleContributorPenalty: round45(singleContributorPenalty)
4905
+ },
4906
+ weight: 0.25,
4907
+ evidence: [
4908
+ {
4909
+ kind: "repository_metric",
4910
+ metric: "ownership.filesWithSingleContributorRatio"
4911
+ }
4912
+ ]
4913
+ },
4914
+ {
4915
+ factorId: "health.ownership.author_entropy",
4916
+ penalty: entropyPenalty,
4917
+ rawMetrics: {
4918
+ authorEntropy: round45(authorEntropy)
4919
+ },
4920
+ normalizedMetrics: {
4921
+ authorEntropyPenalty: round45(entropyPenalty)
4922
+ },
4923
+ weight: 0.25,
4924
+ evidence: [{ kind: "repository_metric", metric: "ownership.authorEntropy" }]
4925
+ },
4926
+ {
4927
+ factorId: "health.ownership.module_single_author_dominance",
4928
+ penalty: moduleDominancePenalty,
4929
+ rawMetrics: {
4930
+ modulesDominatedBySingleContributorRatio: round45(
4931
+ modulesDominatedBySingleContributorRatio
4932
+ )
4933
+ },
4934
+ normalizedMetrics: {
4935
+ moduleDominancePenalty: round45(moduleDominancePenalty)
4936
+ },
4937
+ weight: 0.15,
4938
+ evidence: [{ kind: "repository_metric", metric: "ownership.moduleDominance" }]
4939
+ }
4940
+ ]);
4941
+ const ownershipDistributionPenalty2 = clamp01(
4942
+ ownershipBasePenalty * (0.3 + 0.7 * ownershipReliability) * ownershipPenaltyMultiplier
4943
+ );
4944
+ const ownershipDistributionFactors2 = [
4945
+ {
4946
+ factorId: "health.ownership.top_author_commit_share",
4947
+ penalty: topAuthorPenalty,
4948
+ rawMetrics: {
4949
+ topAuthorCommitShare: round45(topAuthorCommitShare)
4950
+ },
4951
+ normalizedMetrics: {
4952
+ topAuthorPenalty: round45(topAuthorPenalty),
4953
+ ownershipPenaltyMultiplier: round45(ownershipPenaltyMultiplier),
4954
+ ownershipReliability: round45(ownershipReliability)
4955
+ },
4956
+ weight: 0.35,
4957
+ evidence: [{ kind: "repository_metric", metric: "ownership.topAuthorCommitShare" }]
4958
+ },
4959
+ {
4960
+ factorId: "health.ownership.files_with_single_contributor_ratio",
4961
+ penalty: singleContributorPenalty,
4962
+ rawMetrics: {
4963
+ filesWithSingleContributorRatio: round45(filesWithSingleContributorRatio)
4964
+ },
4965
+ normalizedMetrics: {
4966
+ singleContributorPenalty: round45(singleContributorPenalty)
4967
+ },
4968
+ weight: 0.25,
4969
+ evidence: [
4970
+ {
4971
+ kind: "repository_metric",
4972
+ metric: "ownership.filesWithSingleContributorRatio"
4973
+ }
4974
+ ]
4975
+ },
4976
+ {
4977
+ factorId: "health.ownership.author_entropy",
4978
+ penalty: entropyPenalty,
4979
+ rawMetrics: {
4980
+ authorEntropy: round45(authorEntropy)
4981
+ },
4982
+ normalizedMetrics: {
4983
+ authorEntropyPenalty: round45(entropyPenalty)
4984
+ },
4985
+ weight: 0.25,
4986
+ evidence: [{ kind: "repository_metric", metric: "ownership.authorEntropy" }]
4987
+ },
4988
+ {
4989
+ factorId: "health.ownership.module_single_author_dominance",
4990
+ penalty: moduleDominancePenalty,
4991
+ rawMetrics: {
4992
+ modulesDominatedBySingleContributorRatio: round45(
4993
+ modulesDominatedBySingleContributorRatio
4994
+ )
4995
+ },
4996
+ normalizedMetrics: {
4997
+ moduleDominancePenalty: round45(moduleDominancePenalty)
4998
+ },
4999
+ weight: 0.15,
5000
+ evidence: [{ kind: "repository_metric", metric: "ownership.moduleDominance" }]
5001
+ }
5002
+ ];
5003
+ if (topAuthorPenalty >= 0.35) {
5004
+ pushIssue(issues, {
5005
+ id: "health.ownership.top_author_commit_share",
5006
+ ruleId: "ownership.top_author_share",
5007
+ signal: "ownership.topAuthorCommitShare",
5008
+ dimension: "ownershipDistribution",
5009
+ target: input.structural.targetPath,
5010
+ message: "A single contributor owns most commits, concentrating repository knowledge.",
5011
+ severity: topAuthorPenalty >= 0.75 ? "error" : "warn",
5012
+ evidenceMetrics: {
5013
+ topAuthorCommitShare: round45(topAuthorCommitShare),
5014
+ authorEntropy: round45(authorEntropy)
5015
+ },
5016
+ impact: round45(ownershipDistributionPenalty2 * 0.4)
5017
+ });
5018
+ }
5019
+ if (singleContributorPenalty >= 0.35) {
5020
+ pushIssue(issues, {
5021
+ id: "health.ownership.single_author_dominance",
5022
+ ruleId: "ownership.file_dominance",
5023
+ signal: "ownership.filesWithSingleContributorRatio",
5024
+ dimension: "ownershipDistribution",
5025
+ target: input.structural.targetPath,
5026
+ message: "Many files are dominated by a single contributor, reducing change resilience.",
5027
+ evidenceMetrics: {
5028
+ filesWithSingleContributorRatio: round45(filesWithSingleContributorRatio),
5029
+ modulesDominatedBySingleContributorRatio: round45(
5030
+ modulesDominatedBySingleContributorRatio
5031
+ )
5032
+ },
5033
+ impact: round45(ownershipDistributionPenalty2 * 0.35)
5034
+ });
5035
+ }
5036
+ if (entropyPenalty >= 0.35) {
5037
+ pushIssue(issues, {
5038
+ id: "health.ownership.low_author_entropy",
5039
+ ruleId: "ownership.author_entropy",
5040
+ signal: "ownership.authorEntropy",
5041
+ dimension: "ownershipDistribution",
5042
+ target: input.structural.targetPath,
5043
+ message: "Contributor distribution is narrow across the repository.",
5044
+ evidenceMetrics: {
5045
+ authorEntropy: round45(authorEntropy),
5046
+ topAuthorCommitShare: round45(topAuthorCommitShare)
5047
+ },
5048
+ impact: round45(ownershipDistributionPenalty2 * 0.25)
5049
+ });
5050
+ }
5051
+ const modularityHealth2 = clamp01(1 - modularityPenalty);
5052
+ const changeHygieneHealth2 = clamp01(1 - changeHygienePenalty);
5053
+ const testHealthScore2 = clamp01(1 - testHealthPenalty);
5054
+ const ownershipDistributionHealth2 = clamp01(1 - ownershipDistributionPenalty2);
5055
+ const normalizedScore2 = clamp01(
5056
+ modularityHealth2 * DIMENSION_WEIGHTS.modularity + changeHygieneHealth2 * DIMENSION_WEIGHTS.changeHygiene + testHealthScore2 * DIMENSION_WEIGHTS.testHealth + ownershipDistributionHealth2 * DIMENSION_WEIGHTS.ownershipDistribution
5057
+ );
5058
+ const topIssues2 = [...issues].sort(
5059
+ (a, b) => b.impact - a.impact || a.id.localeCompare(b.id) || a.target.localeCompare(b.target)
5060
+ ).slice(0, 12).map(({ impact: _impact, ...issue }) => issue);
5061
+ return {
5062
+ healthScore: toPercentage(normalizedScore2),
5063
+ normalizedScore: round45(normalizedScore2),
5064
+ dimensions: {
5065
+ modularity: toPercentage(modularityHealth2),
5066
+ changeHygiene: toPercentage(changeHygieneHealth2),
5067
+ testHealth: toPercentage(testHealthScore2),
5068
+ ownershipDistribution: toPercentage(ownershipDistributionHealth2)
5069
+ },
5070
+ topIssues: topIssues2,
5071
+ trace: {
5072
+ schemaVersion: HEALTH_TRACE_VERSION,
5073
+ dimensions: [
5074
+ createDimensionTrace("modularity", modularityHealth2, modularityFactors),
5075
+ createDimensionTrace("changeHygiene", changeHygieneHealth2, changeHygieneFactors),
5076
+ createDimensionTrace("testHealth", testHealthScore2, testHealthFactors),
5077
+ createDimensionTrace(
5078
+ "ownershipDistribution",
5079
+ ownershipDistributionHealth2,
5080
+ ownershipDistributionFactors2
5081
+ )
5082
+ ]
5083
+ }
5084
+ };
5085
+ }
5086
+ const ownershipDistributionPenalty = clamp01(0.12 * ownershipPenaltyMultiplier);
5087
+ const ownershipDistributionFactors = [
5088
+ {
5089
+ factorId: "health.ownership.missing_git_history",
5090
+ penalty: ownershipDistributionPenalty,
5091
+ rawMetrics: {
5092
+ gitHistoryAvailable: 0
5093
+ },
5094
+ normalizedMetrics: {
5095
+ ownershipDistributionPenalty: round45(ownershipDistributionPenalty)
5096
+ },
5097
+ weight: 1,
5098
+ evidence: [{ kind: "repository_metric", metric: "evolution.available" }]
5099
+ }
5100
+ ];
5101
+ const modularityHealth = clamp01(1 - modularityPenalty);
5102
+ const changeHygieneHealth = clamp01(1 - changeHygienePenalty);
5103
+ const testHealthScore = clamp01(1 - testHealthPenalty);
5104
+ const ownershipDistributionHealth = clamp01(1 - ownershipDistributionPenalty);
4452
5105
  const normalizedScore = clamp01(
4453
- modularityQuality * DIMENSION_WEIGHTS.modularity + changeHygieneQuality * DIMENSION_WEIGHTS.changeHygiene + testHealthQuality * DIMENSION_WEIGHTS.testHealth
5106
+ modularityHealth * DIMENSION_WEIGHTS.modularity + changeHygieneHealth * DIMENSION_WEIGHTS.changeHygiene + testHealthScore * DIMENSION_WEIGHTS.testHealth + ownershipDistributionHealth * DIMENSION_WEIGHTS.ownershipDistribution
4454
5107
  );
4455
5108
  const topIssues = [...issues].sort(
4456
5109
  (a, b) => b.impact - a.impact || a.id.localeCompare(b.id) || a.target.localeCompare(b.target)
4457
- ).slice(0, 8).map(({ impact: _impact, ...issue }) => issue);
5110
+ ).slice(0, 12).map(({ impact: _impact, ...issue }) => issue);
4458
5111
  return {
4459
- qualityScore: toPercentage(normalizedScore),
5112
+ healthScore: toPercentage(normalizedScore),
4460
5113
  normalizedScore: round45(normalizedScore),
4461
5114
  dimensions: {
4462
- modularity: toPercentage(modularityQuality),
4463
- changeHygiene: toPercentage(changeHygieneQuality),
4464
- testHealth: toPercentage(testHealthQuality)
5115
+ modularity: toPercentage(modularityHealth),
5116
+ changeHygiene: toPercentage(changeHygieneHealth),
5117
+ testHealth: toPercentage(testHealthScore),
5118
+ ownershipDistribution: toPercentage(ownershipDistributionHealth)
4465
5119
  },
4466
- topIssues
5120
+ topIssues,
5121
+ trace: {
5122
+ schemaVersion: HEALTH_TRACE_VERSION,
5123
+ dimensions: [
5124
+ createDimensionTrace("modularity", modularityHealth, modularityFactors),
5125
+ createDimensionTrace("changeHygiene", changeHygieneHealth, changeHygieneFactors),
5126
+ createDimensionTrace("testHealth", testHealthScore, testHealthFactors),
5127
+ createDimensionTrace(
5128
+ "ownershipDistribution",
5129
+ ownershipDistributionHealth,
5130
+ ownershipDistributionFactors
5131
+ )
5132
+ ]
5133
+ }
4467
5134
  };
4468
5135
  };
4469
5136
 
@@ -4640,7 +5307,7 @@ var normalizeWithScale = (value, scale) => {
4640
5307
  }
4641
5308
  return toUnitInterval((value - scale.lower) / (scale.upper - scale.lower));
4642
5309
  };
4643
- var normalizePath2 = (path) => path.replaceAll("\\", "/");
5310
+ var normalizePath3 = (path) => path.replaceAll("\\", "/");
4644
5311
  var computeAggregatorAttenuation = (input) => {
4645
5312
  const { fanIn, fanOut, inCycle, evolutionMetrics, config } = input;
4646
5313
  if (!config.enabled || inCycle > 0) {
@@ -4715,7 +5382,7 @@ var computeEvolutionHistoryConfidence = (structural, evolution, evolutionByFile)
4715
5382
  }
4716
5383
  let coveredFiles = 0;
4717
5384
  for (const file of structural.files) {
4718
- if (evolutionByFile.has(normalizePath2(file.id))) {
5385
+ if (evolutionByFile.has(normalizePath3(file.id))) {
4719
5386
  coveredFiles += 1;
4720
5387
  }
4721
5388
  }
@@ -4909,7 +5576,7 @@ var mapEvolutionByFile = (evolution) => {
4909
5576
  return /* @__PURE__ */ new Map();
4910
5577
  }
4911
5578
  return new Map(
4912
- evolution.files.map((fileMetrics) => [normalizePath2(fileMetrics.filePath), fileMetrics])
5579
+ evolution.files.map((fileMetrics) => [normalizePath3(fileMetrics.filePath), fileMetrics])
4913
5580
  );
4914
5581
  };
4915
5582
  var computeEvolutionScales = (evolutionByFile, config) => {
@@ -4933,7 +5600,7 @@ var computeEvolutionScales = (evolutionByFile, config) => {
4933
5600
  };
4934
5601
  };
4935
5602
  var inferModuleName = (filePath, config) => {
4936
- const normalized = normalizePath2(filePath);
5603
+ const normalized = normalizePath3(filePath);
4937
5604
  const parts = normalized.split("/").filter((part) => part.length > 0);
4938
5605
  if (parts.length <= 1) {
4939
5606
  return config.module.rootLabel;
@@ -4954,7 +5621,7 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
4954
5621
  const clusters = [];
4955
5622
  let cycleClusterCount = 0;
4956
5623
  for (const cycle of structural.cycles) {
4957
- const files = [...new Set(cycle.nodes.map((node) => normalizePath2(node)))].filter(
5624
+ const files = [...new Set(cycle.nodes.map((node) => normalizePath3(node)))].filter(
4958
5625
  (filePath) => fileScoresByFile.has(filePath)
4959
5626
  );
4960
5627
  if (files.length < 2) {
@@ -4986,8 +5653,8 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
4986
5653
  )
4987
5654
  );
4988
5655
  const selectedPairs = candidates.filter((pair) => pair.couplingScore >= threshold).map((pair) => ({
4989
- fileA: normalizePath2(pair.fileA),
4990
- fileB: normalizePath2(pair.fileB),
5656
+ fileA: normalizePath3(pair.fileA),
5657
+ fileB: normalizePath3(pair.fileB),
4991
5658
  couplingScore: pair.couplingScore
4992
5659
  })).filter(
4993
5660
  (pair) => pair.fileA !== pair.fileB && fileScoresByFile.has(pair.fileA) && fileScoresByFile.has(pair.fileB)
@@ -5065,7 +5732,7 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5065
5732
  );
5066
5733
  const evolutionScales = computeEvolutionScales(evolutionByFile, config);
5067
5734
  const cycleFileSet = new Set(
5068
- structural.cycles.flatMap((cycle) => cycle.nodes.map((node) => normalizePath2(node)))
5735
+ structural.cycles.flatMap((cycle) => cycle.nodes.map((node) => normalizePath3(node)))
5069
5736
  );
5070
5737
  const fanInScale = buildQuantileScale(
5071
5738
  structural.files.map((file) => logScale(file.fanIn)),
@@ -5088,7 +5755,7 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
5088
5755
  external: external.available
5089
5756
  });
5090
5757
  const fileRiskContexts = structural.files.map((file) => {
5091
- const filePath = normalizePath2(file.id);
5758
+ const filePath = normalizePath3(file.id);
5092
5759
  const inCycle = cycleFileSet.has(filePath) ? 1 : 0;
5093
5760
  const fanInRisk = normalizeWithScale(logScale(file.fanIn), fanInScale);
5094
5761
  const fanOutRisk = normalizeWithScale(logScale(file.fanOut), fanOutScale);
@@ -5704,31 +6371,9 @@ var evaluateRepositoryRisk = (input, options = {}) => {
5704
6371
  };
5705
6372
  };
5706
6373
 
5707
- // src/application/todo-fixme-counter.ts
5708
- import * as ts2 from "typescript";
5709
- var markerRegex = /\b(?:TODO|FIXME)\b/gi;
5710
- var countMarkers = (text) => text.match(markerRegex)?.length ?? 0;
5711
- var countTodoFixmeInComments = (content) => {
5712
- const scanner = ts2.createScanner(
5713
- ts2.ScriptTarget.Latest,
5714
- false,
5715
- ts2.LanguageVariant.Standard,
5716
- content
5717
- );
5718
- let total = 0;
5719
- let token = scanner.scan();
5720
- while (token !== ts2.SyntaxKind.EndOfFileToken) {
5721
- if (token === ts2.SyntaxKind.SingleLineCommentTrivia || token === ts2.SyntaxKind.MultiLineCommentTrivia) {
5722
- total += countMarkers(scanner.getTokenText());
5723
- }
5724
- token = scanner.scan();
5725
- }
5726
- return total;
5727
- };
5728
-
5729
6374
  // src/application/run-analyze-command.ts
5730
6375
  var resolveTargetPath = (inputPath, cwd) => resolve3(cwd, inputPath ?? ".");
5731
- var riskProfileConfig = {
6376
+ var scoringProfileConfig = {
5732
6377
  default: void 0,
5733
6378
  personal: {
5734
6379
  evolutionFactorWeights: {
@@ -5740,8 +6385,17 @@ var riskProfileConfig = {
5740
6385
  }
5741
6386
  }
5742
6387
  };
5743
- var resolveRiskConfigForProfile = (riskProfile) => {
5744
- return riskProfileConfig[riskProfile ?? "default"];
6388
+ var healthProfileConfig = {
6389
+ default: void 0,
6390
+ personal: {
6391
+ ownershipPenaltyMultiplier: 0.25
6392
+ }
6393
+ };
6394
+ var resolveRiskConfigForProfile = (scoringProfile) => {
6395
+ return scoringProfileConfig[scoringProfile ?? "default"];
6396
+ };
6397
+ var resolveHealthConfigForProfile = (scoringProfile) => {
6398
+ return healthProfileConfig[scoringProfile ?? "default"];
5745
6399
  };
5746
6400
  var createExternalProgressReporter = (logger) => {
5747
6401
  let lastLoggedProgress = 0;
@@ -5854,18 +6508,6 @@ var createEvolutionProgressReporter = (logger) => {
5854
6508
  }
5855
6509
  };
5856
6510
  };
5857
- var collectTodoFixmeCount = async (targetPath, structural) => {
5858
- const filePaths2 = [...structural.files].map((file) => file.relativePath).sort((a, b) => a.localeCompare(b));
5859
- let total = 0;
5860
- for (const relativePath of filePaths2) {
5861
- try {
5862
- const content = await readFile2(join4(targetPath, relativePath), "utf8");
5863
- total += countTodoFixmeInComments(content);
5864
- } catch {
5865
- }
5866
- }
5867
- return total;
5868
- };
5869
6511
  var collectAnalysisInputs = async (inputPath, authorIdentityMode, options = {}, logger = createSilentLogger()) => {
5870
6512
  const invocationCwd = process.env["INIT_CWD"] ?? process.cwd();
5871
6513
  const targetPath = resolveTargetPath(inputPath, invocationCwd);
@@ -5908,14 +6550,10 @@ var collectAnalysisInputs = async (inputPath, authorIdentityMode, options = {},
5908
6550
  } else {
5909
6551
  logger.warn(`external analysis unavailable: ${external.reason}`);
5910
6552
  }
5911
- logger.info("collecting quality text signals");
5912
- const todoFixmeCount = await collectTodoFixmeCount(targetPath, structural);
5913
- logger.debug(`quality text signals: todoFixmeCount=${todoFixmeCount}`);
5914
6553
  return {
5915
6554
  structural,
5916
6555
  evolution,
5917
- external,
5918
- todoFixmeCount
6556
+ external
5919
6557
  };
5920
6558
  };
5921
6559
  var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logger = createSilentLogger()) => {
@@ -5926,32 +6564,33 @@ var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logg
5926
6564
  logger
5927
6565
  );
5928
6566
  logger.info("computing risk summary");
5929
- const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
6567
+ const riskConfig = resolveRiskConfigForProfile(options.scoringProfile);
6568
+ const healthConfig = resolveHealthConfigForProfile(options.scoringProfile);
5930
6569
  const risk = computeRepositoryRiskSummary({
5931
6570
  structural: analysisInputs.structural,
5932
6571
  evolution: analysisInputs.evolution,
5933
6572
  external: analysisInputs.external,
5934
6573
  ...riskConfig === void 0 ? {} : { config: riskConfig }
5935
6574
  });
5936
- const quality = computeRepositoryQualitySummary({
6575
+ const health = computeRepositoryHealthSummary({
5937
6576
  structural: analysisInputs.structural,
5938
6577
  evolution: analysisInputs.evolution,
5939
- todoFixmeCount: analysisInputs.todoFixmeCount
6578
+ ...healthConfig === void 0 ? {} : { config: healthConfig }
5940
6579
  });
5941
6580
  logger.info(
5942
- `analysis completed (riskScore=${risk.riskScore}, qualityScore=${quality.qualityScore})`
6581
+ `analysis completed (riskScore=${risk.riskScore}, healthScore=${health.healthScore})`
5943
6582
  );
5944
6583
  return {
5945
6584
  structural: analysisInputs.structural,
5946
6585
  evolution: analysisInputs.evolution,
5947
6586
  external: analysisInputs.external,
5948
6587
  risk,
5949
- quality
6588
+ health
5950
6589
  };
5951
6590
  };
5952
6591
 
5953
6592
  // src/application/run-check-command.ts
5954
- import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
6593
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
5955
6594
 
5956
6595
  // src/application/build-analysis-snapshot.ts
5957
6596
  var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logger) => {
@@ -5963,7 +6602,8 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
5963
6602
  },
5964
6603
  logger
5965
6604
  );
5966
- const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
6605
+ const riskConfig = resolveRiskConfigForProfile(options.scoringProfile);
6606
+ const healthConfig = resolveHealthConfigForProfile(options.scoringProfile);
5967
6607
  const evaluation = evaluateRepositoryRisk(
5968
6608
  {
5969
6609
  structural: analysisInputs.structural,
@@ -5978,10 +6618,10 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
5978
6618
  evolution: analysisInputs.evolution,
5979
6619
  external: analysisInputs.external,
5980
6620
  risk: evaluation.summary,
5981
- quality: computeRepositoryQualitySummary({
6621
+ health: computeRepositoryHealthSummary({
5982
6622
  structural: analysisInputs.structural,
5983
6623
  evolution: analysisInputs.evolution,
5984
- todoFixmeCount: analysisInputs.todoFixmeCount
6624
+ ...healthConfig === void 0 ? {} : { config: healthConfig }
5985
6625
  })
5986
6626
  };
5987
6627
  return createSnapshot({
@@ -5990,7 +6630,7 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
5990
6630
  analysisConfig: {
5991
6631
  authorIdentityMode,
5992
6632
  includeTrace: options.includeTrace,
5993
- riskProfile: options.riskProfile ?? "default",
6633
+ scoringProfile: options.scoringProfile ?? "default",
5994
6634
  recentWindowDays: analysisInputs.evolution.available ? analysisInputs.evolution.metrics.recentWindowDays : options.recentWindowDays ?? null
5995
6635
  }
5996
6636
  });
@@ -6025,7 +6665,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
6025
6665
  authorIdentityMode,
6026
6666
  {
6027
6667
  includeTrace: options.includeTrace,
6028
- ...options.riskProfile === void 0 ? {} : { riskProfile: options.riskProfile },
6668
+ ...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
6029
6669
  ...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
6030
6670
  },
6031
6671
  logger
@@ -6034,7 +6674,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
6034
6674
  let diff;
6035
6675
  if (options.baselinePath !== void 0) {
6036
6676
  logger.info(`loading baseline snapshot: ${options.baselinePath}`);
6037
- const baselineRaw = await readFile3(options.baselinePath, "utf8");
6677
+ const baselineRaw = await readFile2(options.baselinePath, "utf8");
6038
6678
  try {
6039
6679
  baseline = parseSnapshot(baselineRaw);
6040
6680
  } catch (error) {
@@ -6073,7 +6713,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
6073
6713
  };
6074
6714
 
6075
6715
  // src/application/run-ci-command.ts
6076
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
6716
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
6077
6717
  import { relative as relative2, resolve as resolve4 } from "path";
6078
6718
  var isPathOutsideBase = (value) => {
6079
6719
  return value === ".." || value.startsWith("../") || value.startsWith("..\\");
@@ -6094,7 +6734,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
6094
6734
  authorIdentityMode,
6095
6735
  {
6096
6736
  includeTrace: options.includeTrace,
6097
- ...options.riskProfile === void 0 ? {} : { riskProfile: options.riskProfile },
6737
+ ...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
6098
6738
  ...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
6099
6739
  },
6100
6740
  logger
@@ -6153,7 +6793,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
6153
6793
  authorIdentityMode,
6154
6794
  {
6155
6795
  includeTrace: options.includeTrace,
6156
- ...options.riskProfile === void 0 ? {} : { riskProfile: options.riskProfile },
6796
+ ...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
6157
6797
  ...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
6158
6798
  },
6159
6799
  logger
@@ -6173,7 +6813,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
6173
6813
  diff = compareSnapshots(current, baseline);
6174
6814
  } else if (options.baselinePath !== void 0) {
6175
6815
  logger.info(`loading baseline snapshot: ${options.baselinePath}`);
6176
- const baselineRaw = await readFile4(options.baselinePath, "utf8");
6816
+ const baselineRaw = await readFile3(options.baselinePath, "utf8");
6177
6817
  try {
6178
6818
  baseline = parseSnapshot(baselineRaw);
6179
6819
  } catch (error) {
@@ -6221,7 +6861,7 @@ ${ciMarkdown}`;
6221
6861
  };
6222
6862
 
6223
6863
  // src/application/run-report-command.ts
6224
- import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
6864
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
6225
6865
  var runReportCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
6226
6866
  logger.info("building analysis snapshot");
6227
6867
  const current = await buildAnalysisSnapshot(
@@ -6229,7 +6869,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
6229
6869
  authorIdentityMode,
6230
6870
  {
6231
6871
  includeTrace: options.includeTrace,
6232
- ...options.riskProfile === void 0 ? {} : { riskProfile: options.riskProfile },
6872
+ ...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
6233
6873
  ...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
6234
6874
  },
6235
6875
  logger
@@ -6243,7 +6883,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
6243
6883
  report = createReport(current);
6244
6884
  } else {
6245
6885
  logger.info(`loading baseline snapshot: ${options.comparePath}`);
6246
- const baselineRaw = await readFile5(options.comparePath, "utf8");
6886
+ const baselineRaw = await readFile4(options.comparePath, "utf8");
6247
6887
  const baseline = parseSnapshot(baselineRaw);
6248
6888
  const diff = compareSnapshots(current, baseline);
6249
6889
  report = createReport(current, diff);
@@ -6286,7 +6926,8 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
6286
6926
  logger
6287
6927
  );
6288
6928
  logger.info("computing explainable risk summary");
6289
- const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
6929
+ const riskConfig = resolveRiskConfigForProfile(options.scoringProfile);
6930
+ const healthConfig = resolveHealthConfigForProfile(options.scoringProfile);
6290
6931
  const evaluation = evaluateRepositoryRisk(
6291
6932
  {
6292
6933
  structural: analysisInputs.structural,
@@ -6304,14 +6945,14 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
6304
6945
  evolution: analysisInputs.evolution,
6305
6946
  external: analysisInputs.external,
6306
6947
  risk: evaluation.summary,
6307
- quality: computeRepositoryQualitySummary({
6948
+ health: computeRepositoryHealthSummary({
6308
6949
  structural: analysisInputs.structural,
6309
6950
  evolution: analysisInputs.evolution,
6310
- todoFixmeCount: analysisInputs.todoFixmeCount
6951
+ ...healthConfig === void 0 ? {} : { config: healthConfig }
6311
6952
  })
6312
6953
  };
6313
6954
  logger.info(
6314
- `explanation completed (riskScore=${summary.risk.riskScore}, qualityScore=${summary.quality.qualityScore})`
6955
+ `explanation completed (riskScore=${summary.risk.riskScore}, healthScore=${summary.health.healthScore})`
6315
6956
  );
6316
6957
  return {
6317
6958
  summary,
@@ -6368,7 +7009,7 @@ var renderReportHighlightsText = (report) => {
6368
7009
  lines.push("Repository Summary");
6369
7010
  lines.push(` target: ${report.repository.targetPath}`);
6370
7011
  lines.push(` riskScore: ${report.repository.riskScore}`);
6371
- lines.push(` qualityScore: ${report.quality.qualityScore}`);
7012
+ lines.push(` healthScore: ${report.health.healthScore}`);
6372
7013
  lines.push(` normalizedScore: ${report.repository.normalizedScore}`);
6373
7014
  lines.push(` riskTier: ${report.repository.riskTier}`);
6374
7015
  lines.push("");
@@ -6384,7 +7025,7 @@ var renderReportHighlightsMarkdown = (report) => {
6384
7025
  lines.push("## Repository Summary");
6385
7026
  lines.push(`- target: \`${report.repository.targetPath}\``);
6386
7027
  lines.push(`- riskScore: \`${report.repository.riskScore}\``);
6387
- lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
7028
+ lines.push(`- healthScore: \`${report.health.healthScore}\``);
6388
7029
  lines.push(`- normalizedScore: \`${report.repository.normalizedScore}\``);
6389
7030
  lines.push(`- riskTier: \`${report.repository.riskTier}\``);
6390
7031
  lines.push("");
@@ -6402,7 +7043,7 @@ var renderCompactText = (report, explainSummary) => {
6402
7043
  lines.push("Repository");
6403
7044
  lines.push(` target: ${report.repository.targetPath}`);
6404
7045
  lines.push(` riskScore: ${report.repository.riskScore}`);
6405
- lines.push(` qualityScore: ${report.quality.qualityScore}`);
7046
+ lines.push(` healthScore: ${report.health.healthScore}`);
6406
7047
  lines.push(` riskTier: ${report.repository.riskTier}`);
6407
7048
  lines.push(
6408
7049
  ` dimensions: structural=${report.repository.dimensionScores.structural ?? "n/a"}, evolution=${report.repository.dimensionScores.evolution ?? "n/a"}, external=${report.repository.dimensionScores.external ?? "n/a"}, interactions=${report.repository.dimensionScores.interactions ?? "n/a"}`
@@ -6428,7 +7069,7 @@ var renderCompactMarkdown = (report, explainSummary) => {
6428
7069
  lines.push("## Repository");
6429
7070
  lines.push(`- target: \`${report.repository.targetPath}\``);
6430
7071
  lines.push(`- riskScore: \`${report.repository.riskScore}\``);
6431
- lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
7072
+ lines.push(`- healthScore: \`${report.health.healthScore}\``);
6432
7073
  lines.push(`- riskTier: \`${report.repository.riskTier}\``);
6433
7074
  lines.push(
6434
7075
  `- dimensions: structural=\`${report.repository.dimensionScores.structural ?? "n/a"}\`, evolution=\`${report.repository.dimensionScores.evolution ?? "n/a"}\`, external=\`${report.repository.dimensionScores.external ?? "n/a"}\`, interactions=\`${report.repository.dimensionScores.interactions ?? "n/a"}\``
@@ -6447,12 +7088,12 @@ var renderCompactMarkdown = (report, explainSummary) => {
6447
7088
  }
6448
7089
  return lines.join("\n");
6449
7090
  };
6450
- var riskProfileOption = () => new Option(
6451
- "--risk-profile <profile>",
6452
- "risk profile: default (balanced) or personal (down-weights single-maintainer ownership penalties)"
7091
+ var scoringProfileOption = () => new Option(
7092
+ "--scoring-profile <profile>",
7093
+ "scoring profile: default (balanced) or personal (down-weights single-maintainer ownership penalties for risk and health ownership)"
6453
7094
  ).choices(["default", "personal"]).default("default");
6454
7095
  program.name("codesentinel").description("Structural and evolutionary risk analysis for TypeScript/JavaScript codebases").version(version);
6455
- program.command("analyze").argument("[path]", "path to the project to analyze").addOption(riskProfileOption()).addOption(
7096
+ program.command("analyze").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
6456
7097
  new Option(
6457
7098
  "--author-identity <mode>",
6458
7099
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -6475,7 +7116,7 @@ program.command("analyze").argument("[path]", "path to the project to analyze").
6475
7116
  const summary = await runAnalyzeCommand(
6476
7117
  path,
6477
7118
  options.authorIdentity,
6478
- { recentWindowDays: options.recentWindowDays, riskProfile: options.riskProfile },
7119
+ { recentWindowDays: options.recentWindowDays, scoringProfile: options.scoringProfile },
6479
7120
  logger
6480
7121
  );
6481
7122
  const outputMode = options.json === true ? "json" : options.output;
@@ -6483,7 +7124,7 @@ program.command("analyze").argument("[path]", "path to the project to analyze").
6483
7124
  `);
6484
7125
  }
6485
7126
  );
6486
- program.command("explain").argument("[path]", "path to the project to analyze").addOption(riskProfileOption()).addOption(
7127
+ program.command("explain").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
6487
7128
  new Option(
6488
7129
  "--author-identity <mode>",
6489
7130
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -6509,7 +7150,7 @@ program.command("explain").argument("[path]", "path to the project to analyze").
6509
7150
  ...options.module === void 0 ? {} : { module: options.module },
6510
7151
  top: Number.isFinite(top) ? top : 5,
6511
7152
  recentWindowDays: options.recentWindowDays,
6512
- riskProfile: options.riskProfile,
7153
+ scoringProfile: options.scoringProfile,
6513
7154
  format: options.format
6514
7155
  };
6515
7156
  const result = await runExplainCommand(path, options.authorIdentity, explainOptions, logger);
@@ -6547,7 +7188,7 @@ program.command("dependency-risk").argument("<dependency>", "dependency spec to
6547
7188
  `);
6548
7189
  }
6549
7190
  );
6550
- program.command("report").argument("[path]", "path to the project to analyze").addOption(riskProfileOption()).addOption(
7191
+ program.command("report").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
6551
7192
  new Option(
6552
7193
  "--author-identity <mode>",
6553
7194
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -6576,7 +7217,7 @@ program.command("report").argument("[path]", "path to the project to analyze").a
6576
7217
  ...options.compare === void 0 ? {} : { comparePath: options.compare },
6577
7218
  ...options.snapshot === void 0 ? {} : { snapshotPath: options.snapshot },
6578
7219
  includeTrace: options.trace,
6579
- riskProfile: options.riskProfile,
7220
+ scoringProfile: options.scoringProfile,
6580
7221
  recentWindowDays: options.recentWindowDays
6581
7222
  },
6582
7223
  logger
@@ -6587,7 +7228,7 @@ program.command("report").argument("[path]", "path to the project to analyze").a
6587
7228
  }
6588
7229
  }
6589
7230
  );
6590
- program.command("run").argument("[path]", "path to the project to analyze").addOption(riskProfileOption()).addOption(
7231
+ program.command("run").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
6591
7232
  new Option(
6592
7233
  "--author-identity <mode>",
6593
7234
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -6598,7 +7239,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
6598
7239
  "log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
6599
7240
  ).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
6600
7241
  ).addOption(
6601
- new Option("--format <mode>", "combined output format: text, md, json").choices(["text", "md", "json"]).default("text")
7242
+ new Option("--format <mode>", "combined output format: text, md, json").choices(["text", "md", "json"]).default("md")
6602
7243
  ).addOption(
6603
7244
  new Option("--detail <level>", "run detail level: compact (default), standard, full").choices(["compact", "standard", "full"]).default("compact")
6604
7245
  ).option("--file <path>", "explain a specific file target").option("--module <name>", "explain a specific module target").option("--top <count>", "number of top hotspots to explain when no target is selected", "5").option("--compare <baseline>", "compare against a baseline snapshot JSON file").option("--snapshot <path>", "write current snapshot JSON artifact").option("--no-trace", "disable trace embedding in generated snapshot").addOption(
@@ -6619,7 +7260,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
6619
7260
  top: Number.isFinite(top) ? top : 5,
6620
7261
  format: options.format,
6621
7262
  recentWindowDays: options.recentWindowDays,
6622
- riskProfile: options.riskProfile
7263
+ scoringProfile: options.scoringProfile
6623
7264
  },
6624
7265
  logger
6625
7266
  );
@@ -6633,7 +7274,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
6633
7274
  }
6634
7275
  const report = options.compare === void 0 ? createReport(snapshot) : createReport(
6635
7276
  snapshot,
6636
- compareSnapshots(snapshot, parseSnapshot(await readFile6(options.compare, "utf8")))
7277
+ compareSnapshots(snapshot, parseSnapshot(await readFile5(options.compare, "utf8")))
6637
7278
  );
6638
7279
  if (options.format === "json") {
6639
7280
  const analyzeSummaryOutput = formatAnalyzeOutput(explain.summary, "summary");
@@ -6677,7 +7318,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
6677
7318
  },
6678
7319
  report: {
6679
7320
  repository: report.repository,
6680
- quality: report.quality,
7321
+ health: report.health,
6681
7322
  hotspots: report.hotspots.slice(0, 5),
6682
7323
  structural: report.structural,
6683
7324
  external: report.external
@@ -6825,24 +7466,28 @@ var parseMainBranches = (options) => {
6825
7466
  return unique.length > 0 ? unique : void 0;
6826
7467
  };
6827
7468
  var buildGateConfigFromOptions = (options) => {
6828
- const maxRepoDelta = parseGateNumber(options.maxRepoDelta, "--max-repo-delta");
7469
+ const maxRiskDelta = parseGateNumber(options.maxRiskDelta, "--max-risk-delta");
7470
+ const maxHealthDelta = parseGateNumber(options.maxHealthDelta, "--max-health-delta");
6829
7471
  const maxNewHotspots = parseGateNumber(options.maxNewHotspots, "--max-new-hotspots");
6830
- const maxRepoScore = parseGateNumber(options.maxRepoScore, "--max-repo-score");
7472
+ const maxRiskScore = parseGateNumber(options.maxRiskScore, "--max-risk-score");
7473
+ const minHealthScore = parseGateNumber(options.minHealthScore, "--min-health-score");
6831
7474
  const newHotspotScoreThreshold = parseGateNumber(
6832
7475
  options.newHotspotScoreThreshold,
6833
7476
  "--new-hotspot-score-threshold"
6834
7477
  );
6835
7478
  return {
6836
- ...maxRepoDelta === void 0 ? {} : { maxRepoDelta },
7479
+ ...maxRiskDelta === void 0 ? {} : { maxRiskDelta },
7480
+ ...maxHealthDelta === void 0 ? {} : { maxHealthDelta },
6837
7481
  ...options.noNewCycles === true ? { noNewCycles: true } : {},
6838
7482
  ...options.noNewHighRiskDeps === true ? { noNewHighRiskDeps: true } : {},
6839
7483
  ...maxNewHotspots === void 0 ? {} : { maxNewHotspots },
6840
- ...maxRepoScore === void 0 ? {} : { maxRepoScore },
7484
+ ...maxRiskScore === void 0 ? {} : { maxRiskScore },
7485
+ ...minHealthScore === void 0 ? {} : { minHealthScore },
6841
7486
  ...newHotspotScoreThreshold === void 0 ? {} : { newHotspotScoreThreshold },
6842
7487
  failOn: options.failOn
6843
7488
  };
6844
7489
  };
6845
- program.command("check").argument("[path]", "path to the project to analyze").addOption(riskProfileOption()).addOption(
7490
+ program.command("check").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
6846
7491
  new Option(
6847
7492
  "--author-identity <mode>",
6848
7493
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -6852,7 +7497,10 @@ program.command("check").argument("[path]", "path to the project to analyze").ad
6852
7497
  "--log-level <level>",
6853
7498
  "log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
6854
7499
  ).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
6855
- ).option("--compare <baseline>", "baseline snapshot path").option("--max-repo-delta <value>", "maximum allowed normalized repository score increase").option("--no-new-cycles", "fail if new structural cycles are introduced").option("--no-new-high-risk-deps", "fail if new high-risk direct dependencies are introduced").option("--max-new-hotspots <count>", "maximum allowed number of new hotspots").option("--new-hotspot-score-threshold <score>", "minimum hotspot score to count as new hotspot").option("--max-repo-score <score>", "absolute repository score limit (0..100)").addOption(
7500
+ ).option("--compare <baseline>", "baseline snapshot path").option("--max-risk-delta <value>", "maximum allowed normalized risk score increase").option(
7501
+ "--max-health-delta <value>",
7502
+ "maximum allowed normalized health score regression versus baseline (requires --compare)"
7503
+ ).option("--no-new-cycles", "fail if new structural cycles are introduced").option("--no-new-high-risk-deps", "fail if new high-risk direct dependencies are introduced").option("--max-new-hotspots <count>", "maximum allowed number of new hotspots").option("--new-hotspot-score-threshold <score>", "minimum hotspot score to count as new hotspot").option("--max-risk-score <score>", "absolute risk score limit (0..100)").option("--min-health-score <score>", "minimum health score threshold (0..100)").addOption(
6856
7504
  new Option("--fail-on <level>", "failing severity threshold").choices(["error", "warn"]).default("error")
6857
7505
  ).addOption(
6858
7506
  new Option("--format <mode>", "output format: text, json, md").choices(["text", "json", "md"]).default("text")
@@ -6872,7 +7520,7 @@ program.command("check").argument("[path]", "path to the project to analyze").ad
6872
7520
  {
6873
7521
  ...options.compare === void 0 ? {} : { baselinePath: options.compare },
6874
7522
  includeTrace: options.trace,
6875
- riskProfile: options.riskProfile,
7523
+ scoringProfile: options.scoringProfile,
6876
7524
  recentWindowDays: options.recentWindowDays,
6877
7525
  gateConfig,
6878
7526
  outputFormat: options.format,
@@ -6896,7 +7544,7 @@ program.command("check").argument("[path]", "path to the project to analyze").ad
6896
7544
  }
6897
7545
  }
6898
7546
  );
6899
- program.command("ci").argument("[path]", "path to the project to analyze").addOption(riskProfileOption()).addOption(
7547
+ program.command("ci").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
6900
7548
  new Option(
6901
7549
  "--author-identity <mode>",
6902
7550
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -6920,7 +7568,10 @@ program.command("ci").argument("[path]", "path to the project to analyze").addOp
6920
7568
  ).option(
6921
7569
  "--main-branches <names>",
6922
7570
  "comma-separated default branch candidates for auto baseline resolution (for example: main,master)"
6923
- ).option("--snapshot <path>", "write current snapshot JSON to path").option("--report <path>", "write markdown CI summary report").option("--json-output <path>", "write machine-readable CI JSON output").option("--max-repo-delta <value>", "maximum allowed normalized repository score increase").option("--no-new-cycles", "fail if new structural cycles are introduced").option("--no-new-high-risk-deps", "fail if new high-risk direct dependencies are introduced").option("--max-new-hotspots <count>", "maximum allowed number of new hotspots").option("--new-hotspot-score-threshold <score>", "minimum hotspot score to count as new hotspot").option("--max-repo-score <score>", "absolute repository score limit (0..100)").addOption(
7571
+ ).option("--snapshot <path>", "write current snapshot JSON to path").option("--report <path>", "write markdown CI summary report").option("--json-output <path>", "write machine-readable CI JSON output").option("--max-risk-delta <value>", "maximum allowed normalized risk score increase").option(
7572
+ "--max-health-delta <value>",
7573
+ "maximum allowed normalized health score regression versus baseline"
7574
+ ).option("--no-new-cycles", "fail if new structural cycles are introduced").option("--no-new-high-risk-deps", "fail if new high-risk direct dependencies are introduced").option("--max-new-hotspots <count>", "maximum allowed number of new hotspots").option("--new-hotspot-score-threshold <score>", "minimum hotspot score to count as new hotspot").option("--max-risk-score <score>", "absolute risk score limit (0..100)").option("--min-health-score <score>", "minimum health score threshold (0..100)").addOption(
6924
7575
  new Option("--fail-on <level>", "failing severity threshold").choices(["error", "warn"]).default("error")
6925
7576
  ).option("--no-trace", "disable trace embedding in generated snapshot").addOption(
6926
7577
  new Option(
@@ -6945,7 +7596,7 @@ program.command("ci").argument("[path]", "path to the project to analyze").addOp
6945
7596
  ...options.report === void 0 ? {} : { reportPath: options.report },
6946
7597
  ...options.jsonOutput === void 0 ? {} : { jsonOutputPath: options.jsonOutput },
6947
7598
  includeTrace: options.trace,
6948
- riskProfile: options.riskProfile,
7599
+ scoringProfile: options.scoringProfile,
6949
7600
  recentWindowDays: options.recentWindowDays,
6950
7601
  gateConfig
6951
7602
  },