@getcodesentinel/codesentinel 1.16.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,23 +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(` staticAnalysis: ${report.quality.dimensions.staticAnalysis}`);
1737
- lines.push(` complexity: ${report.quality.dimensions.complexity}`);
1738
- lines.push(` duplication: ${report.quality.dimensions.duplication}`);
1739
- 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}`);
1740
1738
  lines.push(" topIssues:");
1741
- for (const issue of report.quality.topIssues.slice(0, 5)) {
1739
+ for (const issue of report.health.topIssues.slice(0, 5)) {
1742
1740
  const ruleSuffix = issue.ruleId === void 0 ? "" : ` [rule=${issue.ruleId}]`;
1743
1741
  lines.push(
1744
1742
  ` - [${issue.severity}] (${issue.dimension}) ${issue.id}${ruleSuffix} @ ${issue.target}: ${issue.message}`
1745
1743
  );
1746
1744
  }
1747
- if (report.quality.topIssues.length === 0) {
1745
+ if (report.health.topIssues.length === 0) {
1748
1746
  lines.push(" - none");
1749
1747
  }
1750
1748
  lines.push("");
@@ -1823,20 +1821,18 @@ var renderMarkdownReport = (report) => {
1823
1821
  lines.push(`- external: \`${report.repository.dimensionScores.external ?? "n/a"}\``);
1824
1822
  lines.push(`- interactions: \`${report.repository.dimensionScores.interactions ?? "n/a"}\``);
1825
1823
  lines.push("");
1826
- lines.push("## Quality Summary");
1827
- lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
1828
- lines.push(`- normalizedScore: \`${report.quality.normalizedScore}\``);
1829
- lines.push(`- modularity: \`${report.quality.dimensions.modularity}\``);
1830
- lines.push(`- changeHygiene: \`${report.quality.dimensions.changeHygiene}\``);
1831
- lines.push(`- staticAnalysis: \`${report.quality.dimensions.staticAnalysis}\``);
1832
- lines.push(`- complexity: \`${report.quality.dimensions.complexity}\``);
1833
- lines.push(`- duplication: \`${report.quality.dimensions.duplication}\``);
1834
- lines.push(`- testHealth: \`${report.quality.dimensions.testHealth}\``);
1835
- 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) {
1836
1832
  lines.push("- top issues: none");
1837
1833
  } else {
1838
1834
  lines.push("- top issues:");
1839
- for (const issue of report.quality.topIssues.slice(0, 5)) {
1835
+ for (const issue of report.health.topIssues.slice(0, 5)) {
1840
1836
  const ruleSuffix = issue.ruleId === void 0 ? "" : ` [rule=${issue.ruleId}]`;
1841
1837
  lines.push(
1842
1838
  ` - [${issue.severity}] \`${issue.id}\`${ruleSuffix} (\`${issue.dimension}\`) @ \`${issue.target}\`: ${issue.message}`
@@ -1992,8 +1988,8 @@ var validateGateConfig = (input) => {
1992
1988
  if (config.maxRiskDelta !== void 0 && (!Number.isFinite(config.maxRiskDelta) || config.maxRiskDelta < 0)) {
1993
1989
  throw new GovernanceConfigurationError("max-risk-delta must be a finite number >= 0");
1994
1990
  }
1995
- if (config.maxQualityDelta !== void 0 && (!Number.isFinite(config.maxQualityDelta) || config.maxQualityDelta < 0)) {
1996
- throw new GovernanceConfigurationError("max-quality-delta must be a finite number >= 0");
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");
1997
1993
  }
1998
1994
  if (config.maxNewHotspots !== void 0 && (!Number.isInteger(config.maxNewHotspots) || config.maxNewHotspots < 0)) {
1999
1995
  throw new GovernanceConfigurationError("max-new-hotspots must be an integer >= 0");
@@ -2001,8 +1997,8 @@ var validateGateConfig = (input) => {
2001
1997
  if (config.maxRiskScore !== void 0 && (!Number.isFinite(config.maxRiskScore) || config.maxRiskScore < 0 || config.maxRiskScore > 100)) {
2002
1998
  throw new GovernanceConfigurationError("max-risk-score must be a number in [0, 100]");
2003
1999
  }
2004
- if (config.minQualityScore !== void 0 && (!Number.isFinite(config.minQualityScore) || config.minQualityScore < 0 || config.minQualityScore > 100)) {
2005
- throw new GovernanceConfigurationError("min-quality-score must be a number in [0, 100]");
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]");
2006
2002
  }
2007
2003
  if (config.newHotspotScoreThreshold !== void 0 && (!Number.isFinite(config.newHotspotScoreThreshold) || config.newHotspotScoreThreshold < 0 || config.newHotspotScoreThreshold > 100)) {
2008
2004
  throw new GovernanceConfigurationError(
@@ -2030,17 +2026,17 @@ var evaluateGates = (input) => {
2030
2026
  );
2031
2027
  }
2032
2028
  }
2033
- if (config.minQualityScore !== void 0) {
2034
- evaluatedGates.push("min-quality-score");
2035
- const current = input.current.analysis.quality.qualityScore;
2036
- if (current < config.minQualityScore) {
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) {
2037
2033
  violations.push(
2038
2034
  makeViolation(
2039
- "min-quality-score",
2035
+ "min-health-score",
2040
2036
  "error",
2041
- `Quality score ${current} is below configured minimum ${config.minQualityScore}.`,
2037
+ `Health score ${current} is below configured minimum ${config.minHealthScore}.`,
2042
2038
  [input.current.analysis.structural.targetPath],
2043
- [{ kind: "repository_metric", metric: "qualityScore" }]
2039
+ [{ kind: "repository_metric", metric: "healthScore" }]
2044
2040
  )
2045
2041
  );
2046
2042
  }
@@ -2065,22 +2061,22 @@ var evaluateGates = (input) => {
2065
2061
  );
2066
2062
  }
2067
2063
  }
2068
- if (config.maxQualityDelta !== void 0) {
2069
- evaluatedGates.push("max-quality-delta");
2070
- requireDiff(input, "max-quality-delta");
2064
+ if (config.maxHealthDelta !== void 0) {
2065
+ evaluatedGates.push("max-health-delta");
2066
+ requireDiff(input, "max-health-delta");
2071
2067
  const baseline = input.baseline;
2072
2068
  if (baseline === void 0) {
2073
- throw new GovernanceConfigurationError("max-quality-delta requires baseline snapshot");
2069
+ throw new GovernanceConfigurationError("max-health-delta requires baseline snapshot");
2074
2070
  }
2075
- const delta = input.current.analysis.quality.normalizedScore - baseline.analysis.quality.normalizedScore;
2076
- if (delta < -config.maxQualityDelta) {
2071
+ const delta = input.current.analysis.health.normalizedScore - baseline.analysis.health.normalizedScore;
2072
+ if (delta < -config.maxHealthDelta) {
2077
2073
  violations.push(
2078
2074
  makeViolation(
2079
- "max-quality-delta",
2075
+ "max-health-delta",
2080
2076
  "error",
2081
- `Quality normalized score delta ${delta.toFixed(4)} is below allowed minimum ${(-config.maxQualityDelta).toFixed(4)}.`,
2077
+ `Health normalized score delta ${delta.toFixed(4)} is below allowed minimum ${(-config.maxHealthDelta).toFixed(4)}.`,
2082
2078
  [input.current.analysis.structural.targetPath],
2083
- [{ kind: "repository_metric", metric: "qualityNormalizedScore" }]
2079
+ [{ kind: "repository_metric", metric: "healthNormalizedScore" }]
2084
2080
  )
2085
2081
  );
2086
2082
  }
@@ -2542,8 +2538,8 @@ var resolveAutoBaselineRef = async (input) => {
2542
2538
 
2543
2539
  // src/index.ts
2544
2540
  import { readFileSync as readFileSync2 } from "fs";
2545
- import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
2546
- import { dirname as dirname2, resolve as resolve6 } from "path";
2541
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
2542
+ import { dirname as dirname2, resolve as resolve5 } from "path";
2547
2543
  import { fileURLToPath } from "url";
2548
2544
 
2549
2545
  // src/application/format-analyze-output.ts
@@ -2584,11 +2580,11 @@ var createSummaryShape = (summary) => ({
2584
2580
  fragileClusterCount: summary.risk.fragileClusters.length,
2585
2581
  dependencyAmplificationZoneCount: summary.risk.dependencyAmplificationZones.length
2586
2582
  },
2587
- quality: {
2588
- qualityScore: summary.quality.qualityScore,
2589
- normalizedScore: summary.quality.normalizedScore,
2590
- dimensions: summary.quality.dimensions,
2591
- 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)
2592
2588
  }
2593
2589
  });
2594
2590
  var formatAnalyzeOutput = (summary, mode) => mode === "json" ? JSON.stringify(summary, null, 2) : JSON.stringify(createSummaryShape(summary), null, 2);
@@ -3161,7 +3157,7 @@ var promptInstall = async (packageName, latestVersion, currentVersion) => {
3161
3157
  );
3162
3158
  return "skip";
3163
3159
  }
3164
- return await new Promise((resolve7) => {
3160
+ return await new Promise((resolve6) => {
3165
3161
  emitKeypressEvents(stdin);
3166
3162
  let selectedIndex = 0;
3167
3163
  const previousRawMode = stdin.isRaw;
@@ -3186,7 +3182,7 @@ var promptInstall = async (packageName, latestVersion, currentVersion) => {
3186
3182
  } else {
3187
3183
  stderr.write("\n");
3188
3184
  }
3189
- resolve7(choice);
3185
+ resolve6(choice);
3190
3186
  };
3191
3187
  const onKeypress = (_str, key) => {
3192
3188
  if (key.ctrl === true && key.name === "c") {
@@ -3265,7 +3261,7 @@ var checkForCliUpdates = async (input) => {
3265
3261
  };
3266
3262
 
3267
3263
  // src/application/run-analyze-command.ts
3268
- import { resolve as resolve4 } from "path";
3264
+ import { resolve as resolve3 } from "path";
3269
3265
 
3270
3266
  // ../code-graph/dist/index.js
3271
3267
  import { extname, isAbsolute, relative, resolve as resolve2 } from "path";
@@ -4313,539 +4309,7 @@ var analyzeRepositoryEvolutionFromGit = (input, onProgress) => {
4313
4309
  return analyzeRepositoryEvolution(input, historyProvider, onProgress);
4314
4310
  };
4315
4311
 
4316
- // ../quality-signals/dist/index.js
4317
- import { readFile as readFile2 } from "fs/promises";
4318
- import { existsSync as existsSync2 } from "fs";
4319
- import { join as join4, relative as relative2, resolve as resolve3 } from "path";
4320
- import { ESLint } from "eslint";
4321
- import * as ts2 from "typescript";
4322
- import * as ts3 from "typescript";
4323
- var markerRegex = /\b(?:TODO|FIXME)\b/gi;
4324
- var countMarkers = (text) => text.match(markerRegex)?.length ?? 0;
4325
- var countTodoFixmeInComments = (content) => {
4326
- const scanner = ts3.createScanner(
4327
- ts3.ScriptTarget.Latest,
4328
- false,
4329
- ts3.LanguageVariant.Standard,
4330
- content
4331
- );
4332
- let total = 0;
4333
- let token = scanner.scan();
4334
- while (token !== ts3.SyntaxKind.EndOfFileToken) {
4335
- if (token === ts3.SyntaxKind.SingleLineCommentTrivia || token === ts3.SyntaxKind.MultiLineCommentTrivia) {
4336
- total += countMarkers(scanner.getTokenText());
4337
- }
4338
- token = scanner.scan();
4339
- }
4340
- return total;
4341
- };
4342
- var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
4343
- var normalizePath2 = (value) => value.replaceAll("\\", "/");
4344
- var isTestPath = (path) => {
4345
- const normalized = normalizePath2(path);
4346
- return normalized.includes("/__tests__/") || normalized.includes("\\__tests__\\") || normalized.includes(".test.") || normalized.includes(".spec.");
4347
- };
4348
- var collectTodoFixmeCommentCount = async (targetPath, structural) => {
4349
- const filePaths2 = [...structural.files].map((file) => file.relativePath).sort((a, b) => a.localeCompare(b));
4350
- let total = 0;
4351
- for (const relativePath of filePaths2) {
4352
- try {
4353
- const content = await readFile2(join4(targetPath, relativePath), "utf8");
4354
- total += countTodoFixmeInComments(content);
4355
- } catch {
4356
- }
4357
- }
4358
- return total;
4359
- };
4360
- var collectEslintSignals = async (targetPath, structural, logger) => {
4361
- const absoluteFiles = structural.files.map((file) => join4(targetPath, file.relativePath));
4362
- if (absoluteFiles.length === 0) {
4363
- return {
4364
- errorCount: 0,
4365
- warningCount: 0,
4366
- filesWithIssues: 0,
4367
- ruleCounts: []
4368
- };
4369
- }
4370
- try {
4371
- const eslint = new ESLint({ cwd: targetPath, errorOnUnmatchedPattern: false });
4372
- const results = await eslint.lintFiles(absoluteFiles);
4373
- let errorCount = 0;
4374
- let warningCount = 0;
4375
- let filesWithIssues = 0;
4376
- const ruleCounts = /* @__PURE__ */ new Map();
4377
- for (const result of results) {
4378
- if (result.errorCount + result.warningCount > 0) {
4379
- filesWithIssues += 1;
4380
- }
4381
- errorCount += result.errorCount;
4382
- warningCount += result.warningCount;
4383
- for (const message of result.messages) {
4384
- if (message.ruleId === null) {
4385
- continue;
4386
- }
4387
- const severity = message.severity >= 2 ? "error" : "warn";
4388
- const current = ruleCounts.get(message.ruleId);
4389
- if (current === void 0) {
4390
- ruleCounts.set(message.ruleId, {
4391
- ruleId: message.ruleId,
4392
- severity,
4393
- count: 1
4394
- });
4395
- } else {
4396
- ruleCounts.set(message.ruleId, {
4397
- ruleId: current.ruleId,
4398
- severity: current.severity === "error" || severity === "error" ? "error" : "warn",
4399
- count: current.count + 1
4400
- });
4401
- }
4402
- }
4403
- }
4404
- return {
4405
- errorCount,
4406
- warningCount,
4407
- filesWithIssues,
4408
- ruleCounts: [...ruleCounts.values()].sort(
4409
- (a, b) => b.count - a.count || a.ruleId.localeCompare(b.ruleId)
4410
- )
4411
- };
4412
- } catch (error) {
4413
- logger.warn(
4414
- `quality signals: eslint collection unavailable (${error instanceof Error ? error.message : "unknown error"})`
4415
- );
4416
- return void 0;
4417
- }
4418
- };
4419
- var collectTypeScriptSignals = (targetPath, logger) => {
4420
- const tsconfigPath = ts2.findConfigFile(targetPath, ts2.sys.fileExists, "tsconfig.json");
4421
- if (tsconfigPath === void 0) {
4422
- return void 0;
4423
- }
4424
- try {
4425
- const parsed = ts2.getParsedCommandLineOfConfigFile(
4426
- tsconfigPath,
4427
- {},
4428
- {
4429
- ...ts2.sys,
4430
- onUnRecoverableConfigFileDiagnostic: () => {
4431
- throw new Error(`failed to parse ${tsconfigPath}`);
4432
- }
4433
- }
4434
- );
4435
- if (parsed === void 0) {
4436
- return void 0;
4437
- }
4438
- const program2 = ts2.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
4439
- const diagnostics = [
4440
- ...program2.getOptionsDiagnostics(),
4441
- ...program2.getGlobalDiagnostics(),
4442
- ...program2.getSyntacticDiagnostics(),
4443
- ...program2.getSemanticDiagnostics()
4444
- ];
4445
- let errorCount = 0;
4446
- let warningCount = 0;
4447
- const fileSet = /* @__PURE__ */ new Set();
4448
- for (const diagnostic of diagnostics) {
4449
- if (diagnostic.category === ts2.DiagnosticCategory.Error) {
4450
- errorCount += 1;
4451
- } else if (diagnostic.category === ts2.DiagnosticCategory.Warning) {
4452
- warningCount += 1;
4453
- }
4454
- if (diagnostic.file !== void 0) {
4455
- const path = normalizePath2(relative2(targetPath, diagnostic.file.fileName));
4456
- fileSet.add(path);
4457
- }
4458
- }
4459
- return {
4460
- errorCount,
4461
- warningCount,
4462
- filesWithDiagnostics: fileSet.size
4463
- };
4464
- } catch (error) {
4465
- logger.warn(
4466
- `quality signals: typescript diagnostic collection unavailable (${error instanceof Error ? error.message : "unknown error"})`
4467
- );
4468
- return void 0;
4469
- }
4470
- };
4471
- var cyclomaticIncrement = (node) => {
4472
- if (ts2.isIfStatement(node) || ts2.isForStatement(node) || ts2.isForInStatement(node) || ts2.isForOfStatement(node) || ts2.isWhileStatement(node) || ts2.isDoStatement(node) || ts2.isCatchClause(node) || ts2.isConditionalExpression(node)) {
4473
- return 1;
4474
- }
4475
- if (ts2.isCaseClause(node)) {
4476
- return 1;
4477
- }
4478
- if (ts2.isBinaryExpression(node)) {
4479
- if (node.operatorToken.kind === ts2.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts2.SyntaxKind.BarBarToken || node.operatorToken.kind === ts2.SyntaxKind.QuestionQuestionToken) {
4480
- return 1;
4481
- }
4482
- }
4483
- return 0;
4484
- };
4485
- var computeCyclomaticComplexity = (node) => {
4486
- let complexity = 1;
4487
- const visit = (current) => {
4488
- complexity += cyclomaticIncrement(current);
4489
- if (current !== node && (ts2.isFunctionLike(current) || ts2.isArrowFunction(current) || ts2.isMethodDeclaration(current) || ts2.isConstructorDeclaration(current))) {
4490
- return;
4491
- }
4492
- ts2.forEachChild(current, visit);
4493
- };
4494
- visit(node);
4495
- return complexity;
4496
- };
4497
- var collectFunctionComplexities = (content, fileName) => {
4498
- const sourceFile = ts2.createSourceFile(fileName, content, ts2.ScriptTarget.Latest, true);
4499
- const complexities = [];
4500
- const visit = (node) => {
4501
- if (ts2.isFunctionDeclaration(node) || ts2.isMethodDeclaration(node) || ts2.isFunctionExpression(node) || ts2.isArrowFunction(node) || ts2.isConstructorDeclaration(node) || ts2.isGetAccessorDeclaration(node) || ts2.isSetAccessorDeclaration(node)) {
4502
- complexities.push(computeCyclomaticComplexity(node));
4503
- }
4504
- ts2.forEachChild(node, visit);
4505
- };
4506
- visit(sourceFile);
4507
- if (complexities.length === 0) {
4508
- return [computeCyclomaticComplexity(sourceFile)];
4509
- }
4510
- return complexities;
4511
- };
4512
- var collectComplexitySignals = async (targetPath, structural) => {
4513
- const complexities = [];
4514
- for (const file of structural.files) {
4515
- const extension = file.relativePath.slice(file.relativePath.lastIndexOf("."));
4516
- if (!SOURCE_EXTENSIONS2.has(extension)) {
4517
- continue;
4518
- }
4519
- try {
4520
- const content = await readFile2(join4(targetPath, file.relativePath), "utf8");
4521
- complexities.push(...collectFunctionComplexities(content, file.relativePath));
4522
- } catch {
4523
- }
4524
- }
4525
- if (complexities.length === 0) {
4526
- return void 0;
4527
- }
4528
- const averageCyclomatic = complexities.reduce((sum, value) => sum + value, 0) / complexities.length;
4529
- const maxCyclomatic = Math.max(...complexities);
4530
- const highComplexityFileCount = complexities.filter((value) => value >= 15).length;
4531
- return {
4532
- averageCyclomatic,
4533
- maxCyclomatic,
4534
- highComplexityFileCount,
4535
- analyzedFileCount: complexities.length
4536
- };
4537
- };
4538
- var DUPLICATION_MIN_BLOCK_TOKENS = 40;
4539
- var DUPLICATION_KGRAM_TOKENS = 25;
4540
- var DUPLICATION_WINDOW_SIZE = 4;
4541
- var DUPLICATION_MAX_FILES = 5e3;
4542
- var DUPLICATION_MAX_TOKENS_PER_FILE = 12e3;
4543
- var DUPLICATION_MAX_FINGERPRINTS_PER_FILE = 1200;
4544
- var DUPLICATION_EXACT_MAX_WINDOWS = 25e4;
4545
- var HASH_BASE = 16777619;
4546
- var hashString32 = (value) => {
4547
- let hash = 2166136261;
4548
- for (let index = 0; index < value.length; index += 1) {
4549
- hash ^= value.charCodeAt(index);
4550
- hash = Math.imul(hash, 16777619) >>> 0;
4551
- }
4552
- return hash >>> 0;
4553
- };
4554
- var computeRollingBasePower = (kgramSize) => {
4555
- let value = 1;
4556
- for (let index = 1; index < kgramSize; index += 1) {
4557
- value = Math.imul(value, HASH_BASE) >>> 0;
4558
- }
4559
- return value;
4560
- };
4561
- var tokenizeForDuplication = (content, filePath) => {
4562
- const languageVariant = filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? ts2.LanguageVariant.JSX : ts2.LanguageVariant.Standard;
4563
- const scanner = ts2.createScanner(ts2.ScriptTarget.Latest, true, languageVariant, content);
4564
- const tokens = [];
4565
- let token = scanner.scan();
4566
- while (token !== ts2.SyntaxKind.EndOfFileToken) {
4567
- if (token !== ts2.SyntaxKind.WhitespaceTrivia && token !== ts2.SyntaxKind.NewLineTrivia && token !== ts2.SyntaxKind.SingleLineCommentTrivia && token !== ts2.SyntaxKind.MultiLineCommentTrivia) {
4568
- if (token === ts2.SyntaxKind.Identifier || token === ts2.SyntaxKind.PrivateIdentifier) {
4569
- tokens.push("id");
4570
- } else if (token === ts2.SyntaxKind.StringLiteral || token === ts2.SyntaxKind.NoSubstitutionTemplateLiteral || token === ts2.SyntaxKind.TemplateHead || token === ts2.SyntaxKind.TemplateMiddle || token === ts2.SyntaxKind.TemplateTail || token === ts2.SyntaxKind.NumericLiteral || token === ts2.SyntaxKind.BigIntLiteral || token === ts2.SyntaxKind.RegularExpressionLiteral) {
4571
- tokens.push("lit");
4572
- } else {
4573
- const stable = ts2.tokenToString(token) ?? ts2.SyntaxKind[token] ?? `${token}`;
4574
- tokens.push(stable);
4575
- }
4576
- }
4577
- token = scanner.scan();
4578
- }
4579
- return tokens;
4580
- };
4581
- var buildKgramHashes = (tokenValues, kgramSize) => {
4582
- if (tokenValues.length < kgramSize) {
4583
- return [];
4584
- }
4585
- const fingerprints = [];
4586
- const removePower = computeRollingBasePower(kgramSize);
4587
- let hash = 0;
4588
- for (let index = 0; index < kgramSize; index += 1) {
4589
- hash = Math.imul(hash, HASH_BASE) + (tokenValues[index] ?? 0) >>> 0;
4590
- }
4591
- fingerprints.push({ hash, start: 0 });
4592
- for (let start = 1; start <= tokenValues.length - kgramSize; start += 1) {
4593
- const removed = tokenValues[start - 1] ?? 0;
4594
- const added = tokenValues[start + kgramSize - 1] ?? 0;
4595
- const removedContribution = Math.imul(removed, removePower) >>> 0;
4596
- const shifted = Math.imul(hash - removedContribution >>> 0, HASH_BASE) >>> 0;
4597
- hash = shifted + added >>> 0;
4598
- fingerprints.push({ hash, start });
4599
- }
4600
- return fingerprints;
4601
- };
4602
- var winnowFingerprints = (kgrams, windowSize) => {
4603
- if (kgrams.length === 0) {
4604
- return [];
4605
- }
4606
- if (kgrams.length <= windowSize) {
4607
- const minimum = [...kgrams].sort(
4608
- (left, right) => left.hash - right.hash || right.start - left.start
4609
- )[0];
4610
- return minimum === void 0 ? [] : [minimum];
4611
- }
4612
- const selected = /* @__PURE__ */ new Map();
4613
- for (let start = 0; start <= kgrams.length - windowSize; start += 1) {
4614
- let best = kgrams[start];
4615
- if (best === void 0) {
4616
- continue;
4617
- }
4618
- for (let offset = 1; offset < windowSize; offset += 1) {
4619
- const candidate = kgrams[start + offset];
4620
- if (candidate === void 0) {
4621
- continue;
4622
- }
4623
- if (candidate.hash < best.hash || candidate.hash === best.hash && candidate.start > best.start) {
4624
- best = candidate;
4625
- }
4626
- }
4627
- selected.set(`${best.hash}:${best.start}`, best);
4628
- }
4629
- return [...selected.values()].sort((left, right) => left.start - right.start);
4630
- };
4631
- var capFingerprints = (fingerprints, maxFingerprints) => {
4632
- if (fingerprints.length <= maxFingerprints) {
4633
- return fingerprints;
4634
- }
4635
- const step = fingerprints.length / maxFingerprints;
4636
- const capped = [];
4637
- for (let index = 0; index < maxFingerprints; index += 1) {
4638
- const selected = fingerprints[Math.floor(index * step)];
4639
- if (selected !== void 0) {
4640
- capped.push(selected);
4641
- }
4642
- }
4643
- return capped;
4644
- };
4645
- var tokenBlockSignature = (tokens, start, blockLength) => {
4646
- if (start < 0 || start + blockLength > tokens.length) {
4647
- return void 0;
4648
- }
4649
- return tokens.slice(start, start + blockLength).join(" ");
4650
- };
4651
- var mergeTokenRanges = (ranges) => {
4652
- if (ranges.length === 0) {
4653
- return [];
4654
- }
4655
- const sorted = [...ranges].sort(
4656
- (left, right) => left.start - right.start || left.end - right.end
4657
- );
4658
- const merged = [];
4659
- for (const range of sorted) {
4660
- const previous = merged[merged.length - 1];
4661
- if (previous === void 0 || range.start > previous.end) {
4662
- merged.push({ ...range });
4663
- continue;
4664
- }
4665
- previous.end = Math.max(previous.end, range.end);
4666
- }
4667
- return merged;
4668
- };
4669
- var aggregateDuplicationFromSignatures = (signatures, fileByPath) => {
4670
- let duplicatedBlockCount = 0;
4671
- const duplicatedRanges = /* @__PURE__ */ new Map();
4672
- for (const entries of signatures.values()) {
4673
- if (entries.length <= 1) {
4674
- continue;
4675
- }
4676
- const uniqueEntries = /* @__PURE__ */ new Map();
4677
- for (const entry of entries) {
4678
- uniqueEntries.set(`${entry.file}:${entry.start}`, entry);
4679
- }
4680
- if (uniqueEntries.size <= 1) {
4681
- continue;
4682
- }
4683
- duplicatedBlockCount += uniqueEntries.size - 1;
4684
- for (const entry of uniqueEntries.values()) {
4685
- const source = fileByPath.get(entry.file);
4686
- if (source === void 0) {
4687
- continue;
4688
- }
4689
- const signature = tokenBlockSignature(
4690
- source.tokens,
4691
- entry.start,
4692
- DUPLICATION_MIN_BLOCK_TOKENS
4693
- );
4694
- if (signature === void 0) {
4695
- continue;
4696
- }
4697
- const ranges = duplicatedRanges.get(entry.file) ?? [];
4698
- ranges.push({
4699
- start: entry.start,
4700
- end: Math.min(source.tokens.length, entry.start + DUPLICATION_MIN_BLOCK_TOKENS)
4701
- });
4702
- duplicatedRanges.set(entry.file, ranges);
4703
- }
4704
- }
4705
- let duplicatedTokenCount = 0;
4706
- for (const ranges of duplicatedRanges.values()) {
4707
- const mergedRanges = mergeTokenRanges(ranges);
4708
- duplicatedTokenCount += mergedRanges.reduce((sum, range) => sum + (range.end - range.start), 0);
4709
- }
4710
- return {
4711
- duplicatedBlockCount,
4712
- duplicatedTokenCount,
4713
- filesWithDuplication: duplicatedRanges.size
4714
- };
4715
- };
4716
- var collectExactTokenDuplication = (analyzedFiles) => {
4717
- const signatures = /* @__PURE__ */ new Map();
4718
- for (const file of analyzedFiles) {
4719
- const tokenValues = file.tokens.map((token) => hashString32(token));
4720
- const windows = buildKgramHashes(tokenValues, DUPLICATION_MIN_BLOCK_TOKENS);
4721
- for (const window of windows) {
4722
- const signature = tokenBlockSignature(
4723
- file.tokens,
4724
- window.start,
4725
- DUPLICATION_MIN_BLOCK_TOKENS
4726
- );
4727
- if (signature === void 0) {
4728
- continue;
4729
- }
4730
- const entries = signatures.get(signature) ?? [];
4731
- entries.push({ file: file.file, start: window.start });
4732
- signatures.set(signature, entries);
4733
- }
4734
- }
4735
- const fileByPath = new Map(analyzedFiles.map((file) => [file.file, file]));
4736
- return aggregateDuplicationFromSignatures(signatures, fileByPath);
4737
- };
4738
- var collectWinnowingDuplication = (analyzedFiles) => {
4739
- const signatures = /* @__PURE__ */ new Map();
4740
- for (const file of analyzedFiles) {
4741
- const tokenValues = file.tokens.map((token) => hashString32(token));
4742
- const kgrams = buildKgramHashes(tokenValues, DUPLICATION_KGRAM_TOKENS);
4743
- const fingerprints = capFingerprints(
4744
- winnowFingerprints(kgrams, DUPLICATION_WINDOW_SIZE),
4745
- DUPLICATION_MAX_FINGERPRINTS_PER_FILE
4746
- );
4747
- for (const fingerprint of fingerprints) {
4748
- const signature = tokenBlockSignature(
4749
- file.tokens,
4750
- fingerprint.start,
4751
- DUPLICATION_MIN_BLOCK_TOKENS
4752
- );
4753
- if (signature === void 0) {
4754
- continue;
4755
- }
4756
- const entries = signatures.get(signature) ?? [];
4757
- entries.push({ file: file.file, start: fingerprint.start });
4758
- signatures.set(signature, entries);
4759
- }
4760
- }
4761
- const fileByPath = new Map(analyzedFiles.map((file) => [file.file, file]));
4762
- return aggregateDuplicationFromSignatures(signatures, fileByPath);
4763
- };
4764
- var collectDuplicationSignals = async (targetPath, structural) => {
4765
- const files = [...structural.files].map((file) => file.relativePath).sort((left, right) => left.localeCompare(right)).filter((filePath) => SOURCE_EXTENSIONS2.has(filePath.slice(filePath.lastIndexOf(".")))).filter((filePath) => isTestPath(filePath) === false).slice(0, DUPLICATION_MAX_FILES);
4766
- const analyzedFiles = [];
4767
- let significantTokenCount = 0;
4768
- let exactWindowCount = 0;
4769
- for (const relativePath of files) {
4770
- try {
4771
- const content = await readFile2(join4(targetPath, relativePath), "utf8");
4772
- const tokens = tokenizeForDuplication(content, relativePath).slice(
4773
- 0,
4774
- DUPLICATION_MAX_TOKENS_PER_FILE
4775
- );
4776
- significantTokenCount += tokens.length;
4777
- if (tokens.length < DUPLICATION_MIN_BLOCK_TOKENS) {
4778
- continue;
4779
- }
4780
- exactWindowCount += tokens.length - DUPLICATION_MIN_BLOCK_TOKENS + 1;
4781
- analyzedFiles.push({
4782
- file: relativePath,
4783
- tokens
4784
- });
4785
- } catch {
4786
- }
4787
- }
4788
- if (analyzedFiles.length === 0) {
4789
- return void 0;
4790
- }
4791
- const mode = exactWindowCount <= DUPLICATION_EXACT_MAX_WINDOWS ? "exact-token" : "winnowing";
4792
- const aggregated = mode === "exact-token" ? collectExactTokenDuplication(analyzedFiles) : collectWinnowingDuplication(analyzedFiles);
4793
- const duplicatedLineRatio = significantTokenCount === 0 ? 0 : Math.min(1, aggregated.duplicatedTokenCount / significantTokenCount);
4794
- return {
4795
- mode,
4796
- duplicatedLineRatio,
4797
- duplicatedBlockCount: aggregated.duplicatedBlockCount,
4798
- filesWithDuplication: aggregated.filesWithDuplication
4799
- };
4800
- };
4801
- var toRatio = (value) => {
4802
- if (typeof value !== "number" || Number.isFinite(value) === false) {
4803
- return null;
4804
- }
4805
- return Math.min(1, Math.max(0, value / 100));
4806
- };
4807
- var collectCoverageSignals = async (targetPath, logger) => {
4808
- const configuredPath = process.env["CODESENTINEL_QUALITY_COVERAGE_SUMMARY"];
4809
- const summaryPath = configuredPath === void 0 || configuredPath.trim().length === 0 ? join4(targetPath, "coverage", "coverage-summary.json") : resolve3(targetPath, configuredPath);
4810
- if (!existsSync2(summaryPath)) {
4811
- return void 0;
4812
- }
4813
- try {
4814
- const raw = await readFile2(summaryPath, "utf8");
4815
- const parsed = JSON.parse(raw);
4816
- return {
4817
- lineCoverage: toRatio(parsed.total?.lines?.pct),
4818
- branchCoverage: toRatio(parsed.total?.branches?.pct),
4819
- functionCoverage: toRatio(parsed.total?.functions?.pct),
4820
- statementCoverage: toRatio(parsed.total?.statements?.pct)
4821
- };
4822
- } catch (error) {
4823
- logger.warn(
4824
- `quality signals: coverage summary parse failed at ${summaryPath} (${error instanceof Error ? error.message : "unknown error"})`
4825
- );
4826
- return void 0;
4827
- }
4828
- };
4829
- var collectQualitySignals = async (targetPath, structural, logger) => {
4830
- const [todoFixmeCommentCount, eslint, complexity, duplication, coverage] = await Promise.all([
4831
- collectTodoFixmeCommentCount(targetPath, structural),
4832
- collectEslintSignals(targetPath, structural, logger),
4833
- collectComplexitySignals(targetPath, structural),
4834
- collectDuplicationSignals(targetPath, structural),
4835
- collectCoverageSignals(targetPath, logger)
4836
- ]);
4837
- const typescript = collectTypeScriptSignals(targetPath, logger);
4838
- return {
4839
- todoFixmeCommentCount,
4840
- ...eslint === void 0 ? {} : { eslint },
4841
- ...typescript === void 0 ? {} : { typescript },
4842
- ...complexity === void 0 ? {} : { complexity },
4843
- ...duplication === void 0 ? {} : { duplication },
4844
- ...coverage === void 0 ? {} : { coverage }
4845
- };
4846
- };
4847
-
4848
- // ../quality-engine/dist/index.js
4312
+ // ../health-engine/dist/index.js
4849
4313
  var clamp01 = (value) => {
4850
4314
  if (!Number.isFinite(value)) {
4851
4315
  return 0;
@@ -4885,20 +4349,45 @@ var concentration = (rawValues) => {
4885
4349
  return clamp01(normalized);
4886
4350
  };
4887
4351
  var DIMENSION_WEIGHTS = {
4888
- modularity: 0.2,
4889
- changeHygiene: 0.2,
4890
- staticAnalysis: 0.2,
4891
- complexity: 0.15,
4892
- duplication: 0.1,
4893
- testHealth: 0.15
4894
- };
4895
- var QUALITY_TRACE_VERSION = "1";
4896
- var toPercentage = (normalizedQuality) => round45(clamp01(normalizedQuality) * 100);
4897
- var logScaled = (value, scale) => {
4898
- if (scale <= 0) {
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) {
4899
4367
  return 0;
4900
4368
  }
4901
- return clamp01(Math.log1p(Math.max(0, value)) / Math.log1p(scale));
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);
4376
+ };
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));
4902
4391
  };
4903
4392
  var toFactorTrace = (spec) => ({
4904
4393
  factorId: spec.factorId,
@@ -4909,22 +4398,32 @@ var toFactorTrace = (spec) => ({
4909
4398
  weight: round45(spec.weight),
4910
4399
  evidence: spec.evidence
4911
4400
  });
4912
- var createDimensionTrace = (dimension, quality, factors) => ({
4401
+ var createDimensionTrace = (dimension, health, factors) => ({
4913
4402
  dimension,
4914
- normalizedScore: round45(clamp01(quality)),
4915
- score: toPercentage(quality),
4403
+ normalizedScore: round45(clamp01(health)),
4404
+ score: toPercentage(health),
4916
4405
  factors: factors.map((factor) => toFactorTrace(factor))
4917
4406
  });
4918
4407
  var filePaths = (structural) => structural.files.map((file) => file.relativePath);
4919
- var isTestPath2 = (path) => {
4920
- const normalized = path.toLowerCase();
4921
- return normalized.includes("/__tests__/") || normalized.includes("\\__tests__\\") || normalized.includes(".test.") || normalized.includes(".spec.");
4408
+ var normalizePath2 = (value) => value.replaceAll("\\", "/").toLowerCase();
4409
+ var isTestPath = (path) => {
4410
+ const normalized = normalizePath2(path);
4411
+ return normalized.includes("/__tests__/") || normalized.includes("/tests/") || normalized.includes("/test/") || normalized.includes(".test.") || normalized.includes(".spec.");
4922
4412
  };
4923
4413
  var isSourcePath = (path) => {
4924
4414
  if (path.endsWith(".d.ts")) {
4925
4415
  return false;
4926
4416
  }
4927
- return !isTestPath2(path);
4417
+ return !isTestPath(path);
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;
4928
4427
  };
4929
4428
  var pushIssue = (issues, issue) => {
4930
4429
  issues.push({
@@ -4932,484 +4431,704 @@ var pushIssue = (issues, issue) => {
4932
4431
  severity: issue.severity ?? "warn"
4933
4432
  });
4934
4433
  };
4935
- 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);
4936
4437
  const issues = [];
4937
4438
  const sourceFileSet = new Set(input.structural.files.map((file) => file.relativePath));
4938
- const signals = input.signals;
4939
- const cycleCount = input.structural.metrics.cycleCount;
4940
- const cycleSizeAverage = input.structural.cycles.length === 0 ? 0 : average(input.structural.cycles.map((cycle) => cycle.nodes.length));
4941
- const cyclePenalty = clamp01(cycleCount / 6) * 0.7 + clamp01((cycleSizeAverage - 2) / 8) * 0.3;
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
+ );
4942
4456
  const fanInConcentration = concentration(input.structural.files.map((file) => file.fanIn));
4943
4457
  const fanOutConcentration = concentration(input.structural.files.map((file) => file.fanOut));
4944
- const centralityConcentration = average([fanInConcentration, fanOutConcentration]);
4945
- if (cycleCount > 0) {
4946
- pushIssue(issues, {
4947
- id: "quality.modularity.structural_cycles",
4948
- ruleId: "graph.structural_cycles",
4949
- dimension: "modularity",
4950
- target: input.structural.cycles[0]?.nodes.slice().sort((a, b) => a.localeCompare(b)).join(" -> ") ?? input.structural.targetPath,
4951
- message: `${cycleCount} structural cycle(s) increase coupling and refactor cost.`,
4952
- severity: cycleCount >= 3 ? "error" : "warn",
4953
- impact: round45(cyclePenalty * 0.55)
4954
- });
4955
- }
4956
- if (centralityConcentration >= 0.5) {
4957
- const hottest = [...input.structural.files].map((file) => ({
4958
- path: file.relativePath,
4959
- pressure: file.fanIn + file.fanOut
4960
- })).sort((a, b) => b.pressure - a.pressure || a.path.localeCompare(b.path))[0];
4961
- pushIssue(issues, {
4962
- id: "quality.modularity.centrality_concentration",
4963
- ruleId: "graph.centrality_concentration",
4964
- dimension: "modularity",
4965
- target: hottest?.path ?? input.structural.targetPath,
4966
- message: "Fan-in/fan-out pressure is concentrated in a small set of files.",
4967
- impact: round45(centralityConcentration * 0.45)
4968
- });
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));
4969
4479
  }
4970
4480
  const modularityFactors = [
4971
4481
  {
4972
- factorId: "quality.modularity.structural_cycles",
4973
- penalty: cyclePenalty,
4482
+ factorId: "health.modularity.cycle_density",
4483
+ penalty: cycleDensityPenalty,
4974
4484
  rawMetrics: {
4975
- cycleCount,
4976
- averageCycleSize: round45(cycleSizeAverage)
4485
+ cycleCount: input.structural.metrics.cycleCount,
4486
+ cycleEdgeRatio: round45(cycleEdgeRatio),
4487
+ cycleNodeRatio: round45(cycleNodeRatio)
4977
4488
  },
4978
4489
  normalizedMetrics: {
4979
- cyclePenalty: round45(cyclePenalty)
4490
+ cycleDensityPenalty: round45(cycleDensityPenalty)
4980
4491
  },
4981
- weight: 0.55,
4982
- evidence: [{ kind: "repository_metric", metric: "structural.cycles" }]
4492
+ weight: 0.4,
4493
+ evidence: [{ kind: "repository_metric", metric: "structural.cycleEdgeRatio" }]
4983
4494
  },
4984
4495
  {
4985
- factorId: "quality.modularity.centrality_concentration",
4986
- penalty: centralityConcentration,
4496
+ factorId: "health.modularity.fan_concentration",
4497
+ penalty: fanConcentration,
4987
4498
  rawMetrics: {
4988
4499
  fanInConcentration: round45(fanInConcentration),
4989
4500
  fanOutConcentration: round45(fanOutConcentration)
4990
4501
  },
4991
4502
  normalizedMetrics: {
4992
- centralityConcentration: round45(centralityConcentration)
4503
+ fanConcentration: round45(fanConcentration)
4993
4504
  },
4994
- weight: 0.45,
4505
+ weight: 0.25,
4995
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" }]
4996
4531
  }
4997
4532
  ];
4998
- const modularityPenalty = clamp01(
4999
- modularityFactors.reduce((sum, factor) => sum + factor.penalty * factor.weight, 0)
4533
+ const modularityPenalty = dampenForSmallSamples(
4534
+ weightedPenalty(modularityFactors),
4535
+ sourceFileCount,
4536
+ 8,
4537
+ 0.45
5000
4538
  );
5001
- let churnConcentration = 0;
5002
- let volatilityConcentration = 0;
5003
- let couplingDensity = 0;
5004
- let couplingIntensity = 0;
4539
+ if (cycleDensityPenalty >= 0.35) {
4540
+ const firstCycle = input.structural.cycles[0];
4541
+ pushIssue(issues, {
4542
+ id: "health.modularity.cycle_density",
4543
+ ruleId: "graph.cycle_density",
4544
+ signal: "structural.cycleEdgeRatio",
4545
+ dimension: "modularity",
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)
4555
+ });
4556
+ }
4557
+ if (centralityConcentration >= 0.5) {
4558
+ const hottest = [...input.structural.files].map((file) => ({
4559
+ path: file.relativePath,
4560
+ pressure: file.fanIn + file.fanOut
4561
+ })).sort((a, b) => b.pressure - a.pressure || a.path.localeCompare(b.path))[0];
4562
+ pushIssue(issues, {
4563
+ id: "health.modularity.centrality_concentration",
4564
+ ruleId: "graph.centrality_concentration",
4565
+ signal: "structural.centralityPressure",
4566
+ dimension: "modularity",
4567
+ target: hottest?.path ?? input.structural.targetPath,
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)
4574
+ });
4575
+ }
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;
5005
4596
  if (input.evolution.available) {
5006
4597
  const evolutionSourceFiles = input.evolution.files.filter(
5007
4598
  (file) => sourceFileSet.has(file.filePath)
5008
4599
  );
5009
- churnConcentration = concentration(evolutionSourceFiles.map((file) => file.churnTotal));
5010
- volatilityConcentration = concentration(
5011
- 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
5012
4608
  );
5013
- const fileCount = Math.max(1, evolutionSourceFiles.length);
5014
- const maxPairs = fileCount * (fileCount - 1) / 2;
5015
4609
  const sourcePairs = input.evolution.coupling.pairs.filter(
5016
4610
  (pair) => sourceFileSet.has(pair.fileA) && sourceFileSet.has(pair.fileB)
5017
4611
  );
5018
- couplingDensity = maxPairs <= 0 ? 0 : clamp01(sourcePairs.length / maxPairs);
5019
- couplingIntensity = average(sourcePairs.map((pair) => pair.couplingScore));
5020
- 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) {
5021
4634
  const mostChurn = [...evolutionSourceFiles].sort(
5022
4635
  (a, b) => b.churnTotal - a.churnTotal || a.filePath.localeCompare(b.filePath)
5023
4636
  )[0];
5024
4637
  pushIssue(issues, {
5025
- id: "quality.change_hygiene.churn_concentration",
5026
- ruleId: "git.churn_concentration",
4638
+ id: "health.change_hygiene.churn_concentration",
4639
+ ruleId: "git.churn_distribution",
4640
+ signal: "evolution.top10PercentFilesChurnShare",
5027
4641
  dimension: "changeHygiene",
5028
4642
  target: mostChurn?.filePath ?? input.structural.targetPath,
5029
- message: "Churn is concentrated in a narrow part of the codebase.",
5030
- impact: round45(churnConcentration * 0.4)
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)
5031
4648
  });
5032
4649
  }
5033
- if (volatilityConcentration >= 0.45) {
4650
+ if (volatilityConcentrationPenalty >= 0.35) {
5034
4651
  const volatileFile = [...evolutionSourceFiles].sort(
5035
4652
  (a, b) => b.recentVolatility - a.recentVolatility || a.filePath.localeCompare(b.filePath)
5036
4653
  )[0];
5037
4654
  pushIssue(issues, {
5038
- id: "quality.change_hygiene.volatility_concentration",
5039
- ruleId: "git.volatility_concentration",
4655
+ id: "health.change_hygiene.volatility_concentration",
4656
+ ruleId: "git.volatility_distribution",
4657
+ signal: "evolution.top10PercentFilesVolatilityShare",
5040
4658
  dimension: "changeHygiene",
5041
4659
  target: volatileFile?.filePath ?? input.structural.targetPath,
5042
- message: "Recent volatility is concentrated in files that change frequently.",
5043
- impact: round45(volatilityConcentration * 0.3)
4660
+ message: "Recent volatility is concentrated, increasing review and release uncertainty.",
4661
+ evidenceMetrics: {
4662
+ top10PercentFilesVolatilityShare: round45(top10PercentFilesVolatilityShare)
4663
+ },
4664
+ impact: round45(volatilityConcentrationPenalty * 0.3)
5044
4665
  });
5045
4666
  }
5046
- if (couplingDensity >= 0.35 || couplingIntensity >= 0.45) {
4667
+ if (coChangeClusterPenalty >= 0.35) {
5047
4668
  const strongestPair = [...sourcePairs].sort(
5048
4669
  (a, b) => b.couplingScore - a.couplingScore || `${a.fileA}|${a.fileB}`.localeCompare(`${b.fileA}|${b.fileB}`)
5049
4670
  )[0];
5050
4671
  pushIssue(issues, {
5051
- id: "quality.change_hygiene.coupling_density",
5052
- ruleId: "git.coupling_density",
4672
+ id: "health.change_hygiene.dense_co_change_clusters",
4673
+ ruleId: "git.co_change_clusters",
4674
+ signal: "evolution.denseCoChangePairRatio",
5053
4675
  dimension: "changeHygiene",
5054
4676
  target: strongestPair === void 0 ? input.structural.targetPath : `${strongestPair.fileA}<->${strongestPair.fileB}`,
5055
- message: "Co-change relationships are dense, increasing coordination overhead.",
5056
- impact: round45(average([couplingDensity, couplingIntensity]) * 0.3)
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)
5057
4682
  });
5058
4683
  }
5059
4684
  }
5060
- const todoFixmeCommentCount = Math.max(0, signals?.todoFixmeCommentCount ?? 0);
5061
- const todoFixmePenalty = logScaled(todoFixmeCommentCount, 80) * 0.08;
5062
- if (todoFixmeCommentCount > 0) {
5063
- pushIssue(issues, {
5064
- id: "quality.change_hygiene.todo_fixme_load",
5065
- ruleId: "comments.todo_fixme",
5066
- dimension: "changeHygiene",
5067
- target: input.structural.targetPath,
5068
- message: `Found ${todoFixmeCommentCount} TODO/FIXME comment marker(s); cleanup debt is accumulating.`,
5069
- impact: round45(todoFixmePenalty * 0.4)
5070
- });
5071
- }
5072
4685
  const changeHygieneFactors = [
5073
4686
  {
5074
- factorId: "quality.change_hygiene.churn_concentration",
5075
- penalty: churnConcentration,
4687
+ factorId: "health.change_hygiene.churn_concentration",
4688
+ penalty: churnConcentrationPenalty,
5076
4689
  rawMetrics: {
5077
- churnConcentration: round45(churnConcentration)
4690
+ top10PercentFilesChurnShare: round45(top10PercentFilesChurnShare)
5078
4691
  },
5079
4692
  normalizedMetrics: {
5080
- churnConcentration: round45(churnConcentration)
5081
- },
5082
- weight: 0.35,
5083
- evidence: [{ kind: "repository_metric", metric: "evolution.churn" }]
5084
- },
5085
- {
5086
- factorId: "quality.change_hygiene.volatility_concentration",
5087
- penalty: volatilityConcentration,
5088
- rawMetrics: {
5089
- volatilityConcentration: round45(volatilityConcentration)
4693
+ churnConcentrationPenalty: round45(churnConcentrationPenalty)
5090
4694
  },
5091
- normalizedMetrics: {
5092
- volatilityConcentration: round45(volatilityConcentration)
5093
- },
5094
- weight: 0.25,
5095
- evidence: [{ kind: "repository_metric", metric: "evolution.recentVolatility" }]
4695
+ weight: 0.4,
4696
+ evidence: [{ kind: "repository_metric", metric: "evolution.top10PercentFilesChurnShare" }]
5096
4697
  },
5097
4698
  {
5098
- factorId: "quality.change_hygiene.coupling_density",
5099
- penalty: average([couplingDensity, couplingIntensity]),
4699
+ factorId: "health.change_hygiene.volatility_concentration",
4700
+ penalty: volatilityConcentrationPenalty,
5100
4701
  rawMetrics: {
5101
- couplingDensity: round45(couplingDensity),
5102
- couplingIntensity: round45(couplingIntensity)
4702
+ top10PercentFilesVolatilityShare: round45(top10PercentFilesVolatilityShare)
5103
4703
  },
5104
4704
  normalizedMetrics: {
5105
- couplingPressure: round45(average([couplingDensity, couplingIntensity]))
4705
+ volatilityConcentrationPenalty: round45(volatilityConcentrationPenalty)
5106
4706
  },
5107
4707
  weight: 0.3,
5108
- evidence: [{ kind: "repository_metric", metric: "evolution.coupling" }]
4708
+ evidence: [
4709
+ {
4710
+ kind: "repository_metric",
4711
+ metric: "evolution.top10PercentFilesVolatilityShare"
4712
+ }
4713
+ ]
5109
4714
  },
5110
4715
  {
5111
- factorId: "quality.change_hygiene.todo_fixme_load",
5112
- penalty: todoFixmePenalty,
4716
+ factorId: "health.change_hygiene.dense_co_change_clusters",
4717
+ penalty: coChangeClusterPenalty,
5113
4718
  rawMetrics: {
5114
- todoFixmeCommentCount
4719
+ denseCoChangePairRatio: round45(denseCoChangePairRatio)
5115
4720
  },
5116
4721
  normalizedMetrics: {
5117
- todoFixmePenalty: round45(todoFixmePenalty)
4722
+ coChangeClusterPenalty: round45(coChangeClusterPenalty)
5118
4723
  },
5119
- weight: 0.1,
5120
- evidence: [{ kind: "repository_metric", metric: "comments.todo_fixme" }]
4724
+ weight: 0.3,
4725
+ evidence: [{ kind: "repository_metric", metric: "evolution.denseCoChangePairRatio" }]
5121
4726
  }
5122
4727
  ];
5123
- const changeHygienePenalty = input.evolution.available ? clamp01(changeHygieneFactors.reduce((sum, factor) => sum + factor.penalty * factor.weight, 0)) : 0.2;
5124
- const eslint = signals?.eslint;
5125
- const tsc = signals?.typescript;
5126
- const sourceCount = Math.max(1, input.structural.files.length);
5127
- const eslintErrorRate = (eslint?.errorCount ?? 0) / sourceCount;
5128
- const eslintWarnRate = (eslint?.warningCount ?? 0) / sourceCount;
5129
- const tsErrorRate = (tsc?.errorCount ?? 0) / sourceCount;
5130
- const tsWarnRate = (tsc?.warningCount ?? 0) / sourceCount;
5131
- const staticAnalysisFactors = [
5132
- {
5133
- factorId: "quality.static_analysis.eslint_errors",
5134
- penalty: clamp01(eslintErrorRate / 0.5),
5135
- rawMetrics: {
5136
- eslintErrorCount: eslint?.errorCount ?? 0,
5137
- eslintFilesWithIssues: eslint?.filesWithIssues ?? 0
5138
- },
5139
- normalizedMetrics: {
5140
- eslintErrorRate: round45(eslintErrorRate)
5141
- },
5142
- weight: 0.5,
5143
- evidence: [{ kind: "repository_metric", metric: "eslint.errorCount" }]
5144
- },
4728
+ const changeHygienePenalty = input.evolution.available ? weightedPenalty(changeHygieneFactors) : 0.12;
4729
+ const paths = filePaths(input.structural);
4730
+ const testFiles = paths.filter((path) => isTestPath(path)).length;
4731
+ const sourceFiles = paths.filter((path) => isSourcePath(path)).length;
4732
+ const testRatio = sourceFiles <= 0 ? 1 : testFiles / sourceFiles;
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 = [
5145
4738
  {
5146
- factorId: "quality.static_analysis.eslint_warnings",
5147
- penalty: clamp01(eslintWarnRate / 1.2),
4739
+ factorId: "health.test_health.test_file_presence",
4740
+ penalty: testPresencePenalty,
5148
4741
  rawMetrics: {
5149
- eslintWarningCount: eslint?.warningCount ?? 0
4742
+ sourceFiles,
4743
+ testFiles
5150
4744
  },
5151
4745
  normalizedMetrics: {
5152
- eslintWarningRate: round45(eslintWarnRate)
4746
+ testPresencePenalty: round45(testPresencePenalty)
5153
4747
  },
5154
- weight: 0.2,
5155
- evidence: [{ kind: "repository_metric", metric: "eslint.warningCount" }]
4748
+ weight: 0.4,
4749
+ evidence: [{ kind: "repository_metric", metric: "tests.filePresence" }]
5156
4750
  },
5157
4751
  {
5158
- factorId: "quality.static_analysis.typescript_errors",
5159
- penalty: clamp01(tsErrorRate / 0.35),
4752
+ factorId: "health.test_health.test_to_source_ratio",
4753
+ penalty: testRatioPenalty,
5160
4754
  rawMetrics: {
5161
- typeScriptErrorCount: tsc?.errorCount ?? 0,
5162
- typeScriptFilesWithDiagnostics: tsc?.filesWithDiagnostics ?? 0
4755
+ testToSourceRatio: round45(testRatio)
5163
4756
  },
5164
4757
  normalizedMetrics: {
5165
- typeScriptErrorRate: round45(tsErrorRate)
4758
+ testRatioPenalty: round45(testRatioPenalty)
5166
4759
  },
5167
- weight: 0.2,
5168
- evidence: [{ kind: "repository_metric", metric: "typescript.errorCount" }]
4760
+ weight: 0.45,
4761
+ evidence: [{ kind: "repository_metric", metric: "tests.testToSourceRatio" }]
5169
4762
  },
5170
4763
  {
5171
- factorId: "quality.static_analysis.typescript_warnings",
5172
- penalty: clamp01(tsWarnRate / 0.9),
4764
+ factorId: "health.test_health.testing_directory_presence",
4765
+ penalty: testingDirectoryPenalty,
5173
4766
  rawMetrics: {
5174
- typeScriptWarningCount: tsc?.warningCount ?? 0
4767
+ testingDirectoryPresent: testingDirectoryPresent ? 1 : 0
5175
4768
  },
5176
4769
  normalizedMetrics: {
5177
- typeScriptWarningRate: round45(tsWarnRate)
4770
+ testingDirectoryPenalty: round45(testingDirectoryPenalty)
5178
4771
  },
5179
- weight: 0.1,
5180
- evidence: [{ kind: "repository_metric", metric: "typescript.warningCount" }]
4772
+ weight: 0.15,
4773
+ evidence: [{ kind: "repository_metric", metric: "tests.directoryPresence" }]
5181
4774
  }
5182
4775
  ];
5183
- const staticAnalysisPenalty = clamp01(
5184
- staticAnalysisFactors.reduce((sum, factor) => sum + factor.penalty * factor.weight, 0)
4776
+ const testHealthPenalty = dampenForSmallSamples(
4777
+ weightedPenalty(testHealthFactors),
4778
+ sourceFiles,
4779
+ 10,
4780
+ 0.3
5185
4781
  );
5186
- if ((eslint?.errorCount ?? 0) > 0) {
5187
- const topRule = [...eslint?.ruleCounts ?? []].sort(
5188
- (a, b) => b.count - a.count || a.ruleId.localeCompare(b.ruleId)
5189
- )[0];
4782
+ if (sourceFiles > 0 && testFiles === 0) {
5190
4783
  pushIssue(issues, {
5191
- id: "quality.static_analysis.eslint_errors",
5192
- ruleId: topRule?.ruleId ?? "eslint",
5193
- dimension: "staticAnalysis",
4784
+ id: "health.test_health.low_test_presence",
4785
+ ruleId: "tests.file_presence",
4786
+ signal: "tests.filePresence",
4787
+ dimension: "testHealth",
5194
4788
  target: input.structural.targetPath,
5195
- message: topRule === void 0 ? `ESLint reported ${eslint?.errorCount ?? 0} error(s).` : `ESLint reported ${eslint?.errorCount ?? 0} error(s); top rule ${topRule.ruleId} (${topRule.count}).`,
5196
- severity: "error",
5197
- impact: round45(staticAnalysisPenalty * 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)
5198
4797
  });
5199
4798
  }
5200
- if ((tsc?.errorCount ?? 0) > 0) {
4799
+ if (sourceFiles > 0 && testRatio < 0.12) {
5201
4800
  pushIssue(issues, {
5202
- id: "quality.static_analysis.typescript_errors",
5203
- ruleId: "typescript",
5204
- dimension: "staticAnalysis",
4801
+ id: "health.test_health.low_test_ratio",
4802
+ ruleId: "tests.ratio",
4803
+ signal: "tests.testToSourceRatio",
4804
+ dimension: "testHealth",
5205
4805
  target: input.structural.targetPath,
5206
- message: `TypeScript reported ${tsc?.errorCount ?? 0} error diagnostic(s).`,
5207
- severity: "error",
5208
- impact: round45(staticAnalysisPenalty * 0.4)
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)
5209
4813
  });
5210
4814
  }
5211
- const complexity = signals?.complexity;
5212
- const avgComplexity = complexity?.averageCyclomatic ?? 0;
5213
- const maxComplexity = complexity?.maxCyclomatic ?? 0;
5214
- const highComplexityRatio = (complexity?.analyzedFileCount ?? 0) === 0 ? 0 : (complexity?.highComplexityFileCount ?? 0) / Math.max(1, complexity?.analyzedFileCount ?? 1);
5215
- const complexityFactors = [
5216
- {
5217
- factorId: "quality.complexity.average_cyclomatic",
5218
- penalty: clamp01(avgComplexity / 16),
5219
- rawMetrics: {
5220
- averageCyclomatic: round45(avgComplexity)
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" }]
5221
4896
  },
5222
- normalizedMetrics: {
5223
- averageCyclomaticPenalty: round45(clamp01(avgComplexity / 16))
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
+ ]
5224
4913
  },
5225
- weight: 0.4,
5226
- evidence: [{ kind: "repository_metric", metric: "complexity.averageCyclomatic" }]
5227
- },
5228
- {
5229
- factorId: "quality.complexity.max_cyclomatic",
5230
- penalty: clamp01(maxComplexity / 35),
5231
- rawMetrics: {
5232
- maxCyclomatic: round45(maxComplexity)
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" }]
5233
4925
  },
5234
- normalizedMetrics: {
5235
- maxCyclomaticPenalty: round45(clamp01(maxComplexity / 35))
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" }]
5236
4958
  },
5237
- weight: 0.35,
5238
- evidence: [{ kind: "repository_metric", metric: "complexity.maxCyclomatic" }]
5239
- },
5240
- {
5241
- factorId: "quality.complexity.high_complexity_ratio",
5242
- penalty: clamp01(highComplexityRatio / 0.35),
5243
- rawMetrics: {
5244
- highComplexityFileCount: complexity?.highComplexityFileCount ?? 0,
5245
- analyzedFileCount: complexity?.analyzedFileCount ?? 0
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
+ ]
5246
4975
  },
5247
- normalizedMetrics: {
5248
- highComplexityRatio: round45(highComplexityRatio)
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" }]
5249
4987
  },
5250
- weight: 0.25,
5251
- evidence: [{ kind: "repository_metric", metric: "complexity.highComplexityFileCount" }]
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
+ });
5252
5018
  }
5253
- ];
5254
- const complexityPenalty = clamp01(
5255
- complexityFactors.reduce((sum, factor) => sum + factor.penalty * factor.weight, 0)
5256
- );
5257
- if (maxComplexity >= 20 || highComplexityRatio >= 0.2) {
5258
- pushIssue(issues, {
5259
- id: "quality.complexity.high_cyclomatic",
5260
- ruleId: "complexity.cyclomatic",
5261
- dimension: "complexity",
5262
- target: input.structural.targetPath,
5263
- message: `Complexity is elevated (avg=${round45(avgComplexity)}, max=${round45(maxComplexity)}).`,
5264
- impact: round45(complexityPenalty * 0.6)
5265
- });
5266
- }
5267
- const duplication = signals?.duplication;
5268
- const duplicatedLineRatio = duplication?.duplicatedLineRatio ?? 0;
5269
- const duplicatedBlockCount = duplication?.duplicatedBlockCount ?? 0;
5270
- const duplicationFactors = [
5271
- {
5272
- factorId: "quality.duplication.line_ratio",
5273
- penalty: clamp01(duplicatedLineRatio / 0.25),
5274
- rawMetrics: {
5275
- duplicatedLineRatio: round45(duplicatedLineRatio)
5276
- },
5277
- normalizedMetrics: {
5278
- duplicatedLineRatioPenalty: round45(clamp01(duplicatedLineRatio / 0.25))
5279
- },
5280
- weight: 0.7,
5281
- evidence: [{ kind: "repository_metric", metric: "duplication.duplicatedLineRatio" }]
5282
- },
5283
- {
5284
- factorId: "quality.duplication.block_count",
5285
- penalty: logScaled(duplicatedBlockCount, 120),
5286
- rawMetrics: {
5287
- duplicatedBlockCount,
5288
- filesWithDuplication: duplication?.filesWithDuplication ?? 0
5289
- },
5290
- normalizedMetrics: {
5291
- duplicatedBlockPenalty: round45(logScaled(duplicatedBlockCount, 120))
5292
- },
5293
- weight: 0.3,
5294
- evidence: [{ kind: "repository_metric", metric: "duplication.duplicatedBlockCount" }]
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
+ });
5295
5035
  }
5296
- ];
5297
- const duplicationPenalty = clamp01(
5298
- duplicationFactors.reduce((sum, factor) => sum + factor.penalty * factor.weight, 0)
5299
- );
5300
- if (duplicatedLineRatio >= 0.08) {
5301
- pushIssue(issues, {
5302
- id: "quality.duplication.high_duplication",
5303
- ruleId: "duplication.line_ratio",
5304
- dimension: "duplication",
5305
- target: input.structural.targetPath,
5306
- message: `Duplication ratio is high (${toPercentage(duplicatedLineRatio)}%).`,
5307
- impact: round45(duplicationPenalty * 0.6)
5308
- });
5309
- }
5310
- const paths = filePaths(input.structural);
5311
- const testFiles = paths.filter((path) => isTestPath2(path)).length;
5312
- const sourceFiles = paths.filter((path) => isSourcePath(path)).length;
5313
- const testRatio = sourceFiles <= 0 ? 1 : testFiles / sourceFiles;
5314
- const testPresencePenalty = sourceFiles <= 0 ? 0 : 1 - clamp01(testRatio / 0.35);
5315
- const coverageSignals = signals?.coverage;
5316
- const coverageValues = [
5317
- coverageSignals?.lineCoverage,
5318
- coverageSignals?.branchCoverage,
5319
- coverageSignals?.functionCoverage,
5320
- coverageSignals?.statementCoverage
5321
- ].filter((value) => value !== null && value !== void 0);
5322
- const coverageRatio = coverageValues.length === 0 ? null : average(coverageValues);
5323
- const coveragePenalty = coverageRatio === null ? 0.2 : 1 - clamp01(coverageRatio / 0.8);
5324
- const testHealthFactors = [
5325
- {
5326
- factorId: "quality.test_health.test_presence",
5327
- penalty: testPresencePenalty,
5328
- rawMetrics: {
5329
- sourceFiles,
5330
- testFiles,
5331
- testRatio: round45(testRatio)
5332
- },
5333
- normalizedMetrics: {
5334
- testPresencePenalty: round45(testPresencePenalty)
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)
5335
5069
  },
5336
- weight: 0.55,
5337
- evidence: [{ kind: "repository_metric", metric: "tests.file_ratio" }]
5338
- },
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 = [
5339
5088
  {
5340
- factorId: "quality.test_health.coverage",
5341
- penalty: coveragePenalty,
5089
+ factorId: "health.ownership.missing_git_history",
5090
+ penalty: ownershipDistributionPenalty,
5342
5091
  rawMetrics: {
5343
- lineCoverage: coverageSignals?.lineCoverage ?? null,
5344
- branchCoverage: coverageSignals?.branchCoverage ?? null,
5345
- functionCoverage: coverageSignals?.functionCoverage ?? null,
5346
- statementCoverage: coverageSignals?.statementCoverage ?? null
5092
+ gitHistoryAvailable: 0
5347
5093
  },
5348
5094
  normalizedMetrics: {
5349
- coverageRatio: coverageRatio === null ? null : round45(coverageRatio),
5350
- coveragePenalty: round45(coveragePenalty)
5095
+ ownershipDistributionPenalty: round45(ownershipDistributionPenalty)
5351
5096
  },
5352
- weight: 0.45,
5353
- evidence: [{ kind: "repository_metric", metric: "coverage.summary" }]
5097
+ weight: 1,
5098
+ evidence: [{ kind: "repository_metric", metric: "evolution.available" }]
5354
5099
  }
5355
5100
  ];
5356
- const testHealthPenalty = clamp01(
5357
- testHealthFactors.reduce((sum, factor) => sum + factor.penalty * factor.weight, 0)
5358
- );
5359
- if (sourceFiles > 0 && testRatio < 0.2) {
5360
- pushIssue(issues, {
5361
- id: "quality.test_health.low_test_presence",
5362
- ruleId: "tests.file_ratio",
5363
- dimension: "testHealth",
5364
- target: input.structural.targetPath,
5365
- message: `Detected ${testFiles} test file(s) for ${sourceFiles} source file(s).`,
5366
- severity: testRatio === 0 ? "error" : "warn",
5367
- impact: round45(testHealthPenalty * 0.4)
5368
- });
5369
- }
5370
- if (coverageRatio !== null && coverageRatio < 0.6) {
5371
- pushIssue(issues, {
5372
- id: "quality.test_health.low_coverage",
5373
- ruleId: "coverage.threshold",
5374
- dimension: "testHealth",
5375
- target: input.structural.targetPath,
5376
- message: `Coverage is below threshold (${toPercentage(coverageRatio)}%).`,
5377
- impact: round45(testHealthPenalty * 0.35)
5378
- });
5379
- }
5380
- const modularityQuality = clamp01(1 - modularityPenalty);
5381
- const changeHygieneQuality = clamp01(1 - changeHygienePenalty);
5382
- const staticAnalysisQuality = clamp01(1 - staticAnalysisPenalty);
5383
- const complexityQuality = clamp01(1 - complexityPenalty);
5384
- const duplicationQuality = clamp01(1 - duplicationPenalty);
5385
- const testHealthQuality = clamp01(1 - testHealthPenalty);
5101
+ const modularityHealth = clamp01(1 - modularityPenalty);
5102
+ const changeHygieneHealth = clamp01(1 - changeHygienePenalty);
5103
+ const testHealthScore = clamp01(1 - testHealthPenalty);
5104
+ const ownershipDistributionHealth = clamp01(1 - ownershipDistributionPenalty);
5386
5105
  const normalizedScore = clamp01(
5387
- modularityQuality * DIMENSION_WEIGHTS.modularity + changeHygieneQuality * DIMENSION_WEIGHTS.changeHygiene + staticAnalysisQuality * DIMENSION_WEIGHTS.staticAnalysis + complexityQuality * DIMENSION_WEIGHTS.complexity + duplicationQuality * DIMENSION_WEIGHTS.duplication + testHealthQuality * DIMENSION_WEIGHTS.testHealth
5106
+ modularityHealth * DIMENSION_WEIGHTS.modularity + changeHygieneHealth * DIMENSION_WEIGHTS.changeHygiene + testHealthScore * DIMENSION_WEIGHTS.testHealth + ownershipDistributionHealth * DIMENSION_WEIGHTS.ownershipDistribution
5388
5107
  );
5389
5108
  const topIssues = [...issues].sort(
5390
5109
  (a, b) => b.impact - a.impact || a.id.localeCompare(b.id) || a.target.localeCompare(b.target)
5391
5110
  ).slice(0, 12).map(({ impact: _impact, ...issue }) => issue);
5392
5111
  return {
5393
- qualityScore: toPercentage(normalizedScore),
5112
+ healthScore: toPercentage(normalizedScore),
5394
5113
  normalizedScore: round45(normalizedScore),
5395
5114
  dimensions: {
5396
- modularity: toPercentage(modularityQuality),
5397
- changeHygiene: toPercentage(changeHygieneQuality),
5398
- staticAnalysis: toPercentage(staticAnalysisQuality),
5399
- complexity: toPercentage(complexityQuality),
5400
- duplication: toPercentage(duplicationQuality),
5401
- testHealth: toPercentage(testHealthQuality)
5115
+ modularity: toPercentage(modularityHealth),
5116
+ changeHygiene: toPercentage(changeHygieneHealth),
5117
+ testHealth: toPercentage(testHealthScore),
5118
+ ownershipDistribution: toPercentage(ownershipDistributionHealth)
5402
5119
  },
5403
5120
  topIssues,
5404
5121
  trace: {
5405
- schemaVersion: QUALITY_TRACE_VERSION,
5122
+ schemaVersion: HEALTH_TRACE_VERSION,
5406
5123
  dimensions: [
5407
- createDimensionTrace("modularity", modularityQuality, modularityFactors),
5408
- createDimensionTrace("changeHygiene", changeHygieneQuality, changeHygieneFactors),
5409
- createDimensionTrace("staticAnalysis", staticAnalysisQuality, staticAnalysisFactors),
5410
- createDimensionTrace("complexity", complexityQuality, complexityFactors),
5411
- createDimensionTrace("duplication", duplicationQuality, duplicationFactors),
5412
- createDimensionTrace("testHealth", testHealthQuality, testHealthFactors)
5124
+ createDimensionTrace("modularity", modularityHealth, modularityFactors),
5125
+ createDimensionTrace("changeHygiene", changeHygieneHealth, changeHygieneFactors),
5126
+ createDimensionTrace("testHealth", testHealthScore, testHealthFactors),
5127
+ createDimensionTrace(
5128
+ "ownershipDistribution",
5129
+ ownershipDistributionHealth,
5130
+ ownershipDistributionFactors
5131
+ )
5413
5132
  ]
5414
5133
  }
5415
5134
  };
@@ -6653,8 +6372,8 @@ var evaluateRepositoryRisk = (input, options = {}) => {
6653
6372
  };
6654
6373
 
6655
6374
  // src/application/run-analyze-command.ts
6656
- var resolveTargetPath = (inputPath, cwd) => resolve4(cwd, inputPath ?? ".");
6657
- var riskProfileConfig = {
6375
+ var resolveTargetPath = (inputPath, cwd) => resolve3(cwd, inputPath ?? ".");
6376
+ var scoringProfileConfig = {
6658
6377
  default: void 0,
6659
6378
  personal: {
6660
6379
  evolutionFactorWeights: {
@@ -6666,8 +6385,17 @@ var riskProfileConfig = {
6666
6385
  }
6667
6386
  }
6668
6387
  };
6669
- var resolveRiskConfigForProfile = (riskProfile) => {
6670
- 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"];
6671
6399
  };
6672
6400
  var createExternalProgressReporter = (logger) => {
6673
6401
  let lastLoggedProgress = 0;
@@ -6822,16 +6550,10 @@ var collectAnalysisInputs = async (inputPath, authorIdentityMode, options = {},
6822
6550
  } else {
6823
6551
  logger.warn(`external analysis unavailable: ${external.reason}`);
6824
6552
  }
6825
- logger.info("collecting quality signals");
6826
- const qualitySignals = await collectQualitySignals(targetPath, structural, logger);
6827
- logger.debug(
6828
- `quality signals: todoFixmeCommentCount=${qualitySignals.todoFixmeCommentCount ?? 0}, eslintErrors=${qualitySignals.eslint?.errorCount ?? 0}, tscErrors=${qualitySignals.typescript?.errorCount ?? 0}`
6829
- );
6830
6553
  return {
6831
6554
  structural,
6832
6555
  evolution,
6833
- external,
6834
- qualitySignals
6556
+ external
6835
6557
  };
6836
6558
  };
6837
6559
  var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logger = createSilentLogger()) => {
@@ -6842,32 +6564,33 @@ var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logg
6842
6564
  logger
6843
6565
  );
6844
6566
  logger.info("computing risk summary");
6845
- const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
6567
+ const riskConfig = resolveRiskConfigForProfile(options.scoringProfile);
6568
+ const healthConfig = resolveHealthConfigForProfile(options.scoringProfile);
6846
6569
  const risk = computeRepositoryRiskSummary({
6847
6570
  structural: analysisInputs.structural,
6848
6571
  evolution: analysisInputs.evolution,
6849
6572
  external: analysisInputs.external,
6850
6573
  ...riskConfig === void 0 ? {} : { config: riskConfig }
6851
6574
  });
6852
- const quality = computeRepositoryQualitySummary({
6575
+ const health = computeRepositoryHealthSummary({
6853
6576
  structural: analysisInputs.structural,
6854
6577
  evolution: analysisInputs.evolution,
6855
- signals: analysisInputs.qualitySignals
6578
+ ...healthConfig === void 0 ? {} : { config: healthConfig }
6856
6579
  });
6857
6580
  logger.info(
6858
- `analysis completed (riskScore=${risk.riskScore}, qualityScore=${quality.qualityScore})`
6581
+ `analysis completed (riskScore=${risk.riskScore}, healthScore=${health.healthScore})`
6859
6582
  );
6860
6583
  return {
6861
6584
  structural: analysisInputs.structural,
6862
6585
  evolution: analysisInputs.evolution,
6863
6586
  external: analysisInputs.external,
6864
6587
  risk,
6865
- quality
6588
+ health
6866
6589
  };
6867
6590
  };
6868
6591
 
6869
6592
  // src/application/run-check-command.ts
6870
- import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
6593
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
6871
6594
 
6872
6595
  // src/application/build-analysis-snapshot.ts
6873
6596
  var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logger) => {
@@ -6879,7 +6602,8 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
6879
6602
  },
6880
6603
  logger
6881
6604
  );
6882
- const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
6605
+ const riskConfig = resolveRiskConfigForProfile(options.scoringProfile);
6606
+ const healthConfig = resolveHealthConfigForProfile(options.scoringProfile);
6883
6607
  const evaluation = evaluateRepositoryRisk(
6884
6608
  {
6885
6609
  structural: analysisInputs.structural,
@@ -6894,10 +6618,10 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
6894
6618
  evolution: analysisInputs.evolution,
6895
6619
  external: analysisInputs.external,
6896
6620
  risk: evaluation.summary,
6897
- quality: computeRepositoryQualitySummary({
6621
+ health: computeRepositoryHealthSummary({
6898
6622
  structural: analysisInputs.structural,
6899
6623
  evolution: analysisInputs.evolution,
6900
- signals: analysisInputs.qualitySignals
6624
+ ...healthConfig === void 0 ? {} : { config: healthConfig }
6901
6625
  })
6902
6626
  };
6903
6627
  return createSnapshot({
@@ -6906,7 +6630,7 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
6906
6630
  analysisConfig: {
6907
6631
  authorIdentityMode,
6908
6632
  includeTrace: options.includeTrace,
6909
- riskProfile: options.riskProfile ?? "default",
6633
+ scoringProfile: options.scoringProfile ?? "default",
6910
6634
  recentWindowDays: analysisInputs.evolution.available ? analysisInputs.evolution.metrics.recentWindowDays : options.recentWindowDays ?? null
6911
6635
  }
6912
6636
  });
@@ -6941,7 +6665,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
6941
6665
  authorIdentityMode,
6942
6666
  {
6943
6667
  includeTrace: options.includeTrace,
6944
- ...options.riskProfile === void 0 ? {} : { riskProfile: options.riskProfile },
6668
+ ...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
6945
6669
  ...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
6946
6670
  },
6947
6671
  logger
@@ -6950,7 +6674,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
6950
6674
  let diff;
6951
6675
  if (options.baselinePath !== void 0) {
6952
6676
  logger.info(`loading baseline snapshot: ${options.baselinePath}`);
6953
- const baselineRaw = await readFile3(options.baselinePath, "utf8");
6677
+ const baselineRaw = await readFile2(options.baselinePath, "utf8");
6954
6678
  try {
6955
6679
  baseline = parseSnapshot(baselineRaw);
6956
6680
  } catch (error) {
@@ -6989,8 +6713,8 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
6989
6713
  };
6990
6714
 
6991
6715
  // src/application/run-ci-command.ts
6992
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
6993
- import { relative as relative3, resolve as resolve5 } from "path";
6716
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
6717
+ import { relative as relative2, resolve as resolve4 } from "path";
6994
6718
  var isPathOutsideBase = (value) => {
6995
6719
  return value === ".." || value.startsWith("../") || value.startsWith("..\\");
6996
6720
  };
@@ -7003,14 +6727,14 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
7003
6727
  if (options.baselineSha !== void 0 && options.baselineRef !== "auto") {
7004
6728
  throw new GovernanceConfigurationError("baseline-sha requires --baseline-ref auto");
7005
6729
  }
7006
- const resolvedTargetPath = resolve5(inputPath ?? process.cwd());
6730
+ const resolvedTargetPath = resolve4(inputPath ?? process.cwd());
7007
6731
  logger.info("building current snapshot");
7008
6732
  const current = await buildAnalysisSnapshot(
7009
6733
  inputPath,
7010
6734
  authorIdentityMode,
7011
6735
  {
7012
6736
  includeTrace: options.includeTrace,
7013
- ...options.riskProfile === void 0 ? {} : { riskProfile: options.riskProfile },
6737
+ ...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
7014
6738
  ...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
7015
6739
  },
7016
6740
  logger
@@ -7057,19 +6781,19 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
7057
6781
  repositoryPath: resolvedTargetPath,
7058
6782
  baselineRef,
7059
6783
  analyzeWorktree: async (worktreePath, repositoryRoot) => {
7060
- const relativeTargetPath = relative3(repositoryRoot, resolvedTargetPath);
6784
+ const relativeTargetPath = relative2(repositoryRoot, resolvedTargetPath);
7061
6785
  if (isPathOutsideBase(relativeTargetPath)) {
7062
6786
  throw new GovernanceConfigurationError(
7063
6787
  `target path is outside git repository root: ${resolvedTargetPath}`
7064
6788
  );
7065
6789
  }
7066
- const baselineTargetPath = relativeTargetPath.length === 0 || relativeTargetPath === "." ? worktreePath : resolve5(worktreePath, relativeTargetPath);
6790
+ const baselineTargetPath = relativeTargetPath.length === 0 || relativeTargetPath === "." ? worktreePath : resolve4(worktreePath, relativeTargetPath);
7067
6791
  return buildAnalysisSnapshot(
7068
6792
  baselineTargetPath,
7069
6793
  authorIdentityMode,
7070
6794
  {
7071
6795
  includeTrace: options.includeTrace,
7072
- ...options.riskProfile === void 0 ? {} : { riskProfile: options.riskProfile },
6796
+ ...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
7073
6797
  ...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
7074
6798
  },
7075
6799
  logger
@@ -7089,7 +6813,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
7089
6813
  diff = compareSnapshots(current, baseline);
7090
6814
  } else if (options.baselinePath !== void 0) {
7091
6815
  logger.info(`loading baseline snapshot: ${options.baselinePath}`);
7092
- const baselineRaw = await readFile4(options.baselinePath, "utf8");
6816
+ const baselineRaw = await readFile3(options.baselinePath, "utf8");
7093
6817
  try {
7094
6818
  baseline = parseSnapshot(baselineRaw);
7095
6819
  } catch (error) {
@@ -7137,7 +6861,7 @@ ${ciMarkdown}`;
7137
6861
  };
7138
6862
 
7139
6863
  // src/application/run-report-command.ts
7140
- import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
6864
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
7141
6865
  var runReportCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
7142
6866
  logger.info("building analysis snapshot");
7143
6867
  const current = await buildAnalysisSnapshot(
@@ -7145,7 +6869,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
7145
6869
  authorIdentityMode,
7146
6870
  {
7147
6871
  includeTrace: options.includeTrace,
7148
- ...options.riskProfile === void 0 ? {} : { riskProfile: options.riskProfile },
6872
+ ...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
7149
6873
  ...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
7150
6874
  },
7151
6875
  logger
@@ -7159,7 +6883,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
7159
6883
  report = createReport(current);
7160
6884
  } else {
7161
6885
  logger.info(`loading baseline snapshot: ${options.comparePath}`);
7162
- const baselineRaw = await readFile5(options.comparePath, "utf8");
6886
+ const baselineRaw = await readFile4(options.comparePath, "utf8");
7163
6887
  const baseline = parseSnapshot(baselineRaw);
7164
6888
  const diff = compareSnapshots(current, baseline);
7165
6889
  report = createReport(current, diff);
@@ -7202,7 +6926,8 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
7202
6926
  logger
7203
6927
  );
7204
6928
  logger.info("computing explainable risk summary");
7205
- const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
6929
+ const riskConfig = resolveRiskConfigForProfile(options.scoringProfile);
6930
+ const healthConfig = resolveHealthConfigForProfile(options.scoringProfile);
7206
6931
  const evaluation = evaluateRepositoryRisk(
7207
6932
  {
7208
6933
  structural: analysisInputs.structural,
@@ -7220,14 +6945,14 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
7220
6945
  evolution: analysisInputs.evolution,
7221
6946
  external: analysisInputs.external,
7222
6947
  risk: evaluation.summary,
7223
- quality: computeRepositoryQualitySummary({
6948
+ health: computeRepositoryHealthSummary({
7224
6949
  structural: analysisInputs.structural,
7225
6950
  evolution: analysisInputs.evolution,
7226
- signals: analysisInputs.qualitySignals
6951
+ ...healthConfig === void 0 ? {} : { config: healthConfig }
7227
6952
  })
7228
6953
  };
7229
6954
  logger.info(
7230
- `explanation completed (riskScore=${summary.risk.riskScore}, qualityScore=${summary.quality.qualityScore})`
6955
+ `explanation completed (riskScore=${summary.risk.riskScore}, healthScore=${summary.health.healthScore})`
7231
6956
  );
7232
6957
  return {
7233
6958
  summary,
@@ -7238,7 +6963,7 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
7238
6963
 
7239
6964
  // src/index.ts
7240
6965
  var program = new Command();
7241
- var packageJsonPath = resolve6(dirname2(fileURLToPath(import.meta.url)), "../package.json");
6966
+ var packageJsonPath = resolve5(dirname2(fileURLToPath(import.meta.url)), "../package.json");
7242
6967
  var { version } = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
7243
6968
  var parseRecentWindowDays = (value) => {
7244
6969
  const parsed = Number.parseInt(value, 10);
@@ -7284,7 +7009,7 @@ var renderReportHighlightsText = (report) => {
7284
7009
  lines.push("Repository Summary");
7285
7010
  lines.push(` target: ${report.repository.targetPath}`);
7286
7011
  lines.push(` riskScore: ${report.repository.riskScore}`);
7287
- lines.push(` qualityScore: ${report.quality.qualityScore}`);
7012
+ lines.push(` healthScore: ${report.health.healthScore}`);
7288
7013
  lines.push(` normalizedScore: ${report.repository.normalizedScore}`);
7289
7014
  lines.push(` riskTier: ${report.repository.riskTier}`);
7290
7015
  lines.push("");
@@ -7300,7 +7025,7 @@ var renderReportHighlightsMarkdown = (report) => {
7300
7025
  lines.push("## Repository Summary");
7301
7026
  lines.push(`- target: \`${report.repository.targetPath}\``);
7302
7027
  lines.push(`- riskScore: \`${report.repository.riskScore}\``);
7303
- lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
7028
+ lines.push(`- healthScore: \`${report.health.healthScore}\``);
7304
7029
  lines.push(`- normalizedScore: \`${report.repository.normalizedScore}\``);
7305
7030
  lines.push(`- riskTier: \`${report.repository.riskTier}\``);
7306
7031
  lines.push("");
@@ -7318,7 +7043,7 @@ var renderCompactText = (report, explainSummary) => {
7318
7043
  lines.push("Repository");
7319
7044
  lines.push(` target: ${report.repository.targetPath}`);
7320
7045
  lines.push(` riskScore: ${report.repository.riskScore}`);
7321
- lines.push(` qualityScore: ${report.quality.qualityScore}`);
7046
+ lines.push(` healthScore: ${report.health.healthScore}`);
7322
7047
  lines.push(` riskTier: ${report.repository.riskTier}`);
7323
7048
  lines.push(
7324
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"}`
@@ -7344,7 +7069,7 @@ var renderCompactMarkdown = (report, explainSummary) => {
7344
7069
  lines.push("## Repository");
7345
7070
  lines.push(`- target: \`${report.repository.targetPath}\``);
7346
7071
  lines.push(`- riskScore: \`${report.repository.riskScore}\``);
7347
- lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
7072
+ lines.push(`- healthScore: \`${report.health.healthScore}\``);
7348
7073
  lines.push(`- riskTier: \`${report.repository.riskTier}\``);
7349
7074
  lines.push(
7350
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"}\``
@@ -7363,12 +7088,12 @@ var renderCompactMarkdown = (report, explainSummary) => {
7363
7088
  }
7364
7089
  return lines.join("\n");
7365
7090
  };
7366
- var riskProfileOption = () => new Option(
7367
- "--risk-profile <profile>",
7368
- "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)"
7369
7094
  ).choices(["default", "personal"]).default("default");
7370
7095
  program.name("codesentinel").description("Structural and evolutionary risk analysis for TypeScript/JavaScript codebases").version(version);
7371
- 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(
7372
7097
  new Option(
7373
7098
  "--author-identity <mode>",
7374
7099
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -7391,7 +7116,7 @@ program.command("analyze").argument("[path]", "path to the project to analyze").
7391
7116
  const summary = await runAnalyzeCommand(
7392
7117
  path,
7393
7118
  options.authorIdentity,
7394
- { recentWindowDays: options.recentWindowDays, riskProfile: options.riskProfile },
7119
+ { recentWindowDays: options.recentWindowDays, scoringProfile: options.scoringProfile },
7395
7120
  logger
7396
7121
  );
7397
7122
  const outputMode = options.json === true ? "json" : options.output;
@@ -7399,7 +7124,7 @@ program.command("analyze").argument("[path]", "path to the project to analyze").
7399
7124
  `);
7400
7125
  }
7401
7126
  );
7402
- 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(
7403
7128
  new Option(
7404
7129
  "--author-identity <mode>",
7405
7130
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -7425,7 +7150,7 @@ program.command("explain").argument("[path]", "path to the project to analyze").
7425
7150
  ...options.module === void 0 ? {} : { module: options.module },
7426
7151
  top: Number.isFinite(top) ? top : 5,
7427
7152
  recentWindowDays: options.recentWindowDays,
7428
- riskProfile: options.riskProfile,
7153
+ scoringProfile: options.scoringProfile,
7429
7154
  format: options.format
7430
7155
  };
7431
7156
  const result = await runExplainCommand(path, options.authorIdentity, explainOptions, logger);
@@ -7463,7 +7188,7 @@ program.command("dependency-risk").argument("<dependency>", "dependency spec to
7463
7188
  `);
7464
7189
  }
7465
7190
  );
7466
- 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(
7467
7192
  new Option(
7468
7193
  "--author-identity <mode>",
7469
7194
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -7492,7 +7217,7 @@ program.command("report").argument("[path]", "path to the project to analyze").a
7492
7217
  ...options.compare === void 0 ? {} : { comparePath: options.compare },
7493
7218
  ...options.snapshot === void 0 ? {} : { snapshotPath: options.snapshot },
7494
7219
  includeTrace: options.trace,
7495
- riskProfile: options.riskProfile,
7220
+ scoringProfile: options.scoringProfile,
7496
7221
  recentWindowDays: options.recentWindowDays
7497
7222
  },
7498
7223
  logger
@@ -7503,7 +7228,7 @@ program.command("report").argument("[path]", "path to the project to analyze").a
7503
7228
  }
7504
7229
  }
7505
7230
  );
7506
- 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(
7507
7232
  new Option(
7508
7233
  "--author-identity <mode>",
7509
7234
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -7535,7 +7260,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
7535
7260
  top: Number.isFinite(top) ? top : 5,
7536
7261
  format: options.format,
7537
7262
  recentWindowDays: options.recentWindowDays,
7538
- riskProfile: options.riskProfile
7263
+ scoringProfile: options.scoringProfile
7539
7264
  },
7540
7265
  logger
7541
7266
  );
@@ -7549,7 +7274,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
7549
7274
  }
7550
7275
  const report = options.compare === void 0 ? createReport(snapshot) : createReport(
7551
7276
  snapshot,
7552
- compareSnapshots(snapshot, parseSnapshot(await readFile6(options.compare, "utf8")))
7277
+ compareSnapshots(snapshot, parseSnapshot(await readFile5(options.compare, "utf8")))
7553
7278
  );
7554
7279
  if (options.format === "json") {
7555
7280
  const analyzeSummaryOutput = formatAnalyzeOutput(explain.summary, "summary");
@@ -7593,7 +7318,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
7593
7318
  },
7594
7319
  report: {
7595
7320
  repository: report.repository,
7596
- quality: report.quality,
7321
+ health: report.health,
7597
7322
  hotspots: report.hotspots.slice(0, 5),
7598
7323
  structural: report.structural,
7599
7324
  external: report.external
@@ -7742,27 +7467,27 @@ var parseMainBranches = (options) => {
7742
7467
  };
7743
7468
  var buildGateConfigFromOptions = (options) => {
7744
7469
  const maxRiskDelta = parseGateNumber(options.maxRiskDelta, "--max-risk-delta");
7745
- const maxQualityDelta = parseGateNumber(options.maxQualityDelta, "--max-quality-delta");
7470
+ const maxHealthDelta = parseGateNumber(options.maxHealthDelta, "--max-health-delta");
7746
7471
  const maxNewHotspots = parseGateNumber(options.maxNewHotspots, "--max-new-hotspots");
7747
7472
  const maxRiskScore = parseGateNumber(options.maxRiskScore, "--max-risk-score");
7748
- const minQualityScore = parseGateNumber(options.minQualityScore, "--min-quality-score");
7473
+ const minHealthScore = parseGateNumber(options.minHealthScore, "--min-health-score");
7749
7474
  const newHotspotScoreThreshold = parseGateNumber(
7750
7475
  options.newHotspotScoreThreshold,
7751
7476
  "--new-hotspot-score-threshold"
7752
7477
  );
7753
7478
  return {
7754
7479
  ...maxRiskDelta === void 0 ? {} : { maxRiskDelta },
7755
- ...maxQualityDelta === void 0 ? {} : { maxQualityDelta },
7480
+ ...maxHealthDelta === void 0 ? {} : { maxHealthDelta },
7756
7481
  ...options.noNewCycles === true ? { noNewCycles: true } : {},
7757
7482
  ...options.noNewHighRiskDeps === true ? { noNewHighRiskDeps: true } : {},
7758
7483
  ...maxNewHotspots === void 0 ? {} : { maxNewHotspots },
7759
7484
  ...maxRiskScore === void 0 ? {} : { maxRiskScore },
7760
- ...minQualityScore === void 0 ? {} : { minQualityScore },
7485
+ ...minHealthScore === void 0 ? {} : { minHealthScore },
7761
7486
  ...newHotspotScoreThreshold === void 0 ? {} : { newHotspotScoreThreshold },
7762
7487
  failOn: options.failOn
7763
7488
  };
7764
7489
  };
7765
- 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(
7766
7491
  new Option(
7767
7492
  "--author-identity <mode>",
7768
7493
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -7773,9 +7498,9 @@ program.command("check").argument("[path]", "path to the project to analyze").ad
7773
7498
  "log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
7774
7499
  ).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
7775
7500
  ).option("--compare <baseline>", "baseline snapshot path").option("--max-risk-delta <value>", "maximum allowed normalized risk score increase").option(
7776
- "--max-quality-delta <value>",
7777
- "maximum allowed normalized quality score regression versus baseline (requires --compare)"
7778
- ).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-quality-score <score>", "minimum quality score threshold (0..100)").addOption(
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(
7779
7504
  new Option("--fail-on <level>", "failing severity threshold").choices(["error", "warn"]).default("error")
7780
7505
  ).addOption(
7781
7506
  new Option("--format <mode>", "output format: text, json, md").choices(["text", "json", "md"]).default("text")
@@ -7795,7 +7520,7 @@ program.command("check").argument("[path]", "path to the project to analyze").ad
7795
7520
  {
7796
7521
  ...options.compare === void 0 ? {} : { baselinePath: options.compare },
7797
7522
  includeTrace: options.trace,
7798
- riskProfile: options.riskProfile,
7523
+ scoringProfile: options.scoringProfile,
7799
7524
  recentWindowDays: options.recentWindowDays,
7800
7525
  gateConfig,
7801
7526
  outputFormat: options.format,
@@ -7819,7 +7544,7 @@ program.command("check").argument("[path]", "path to the project to analyze").ad
7819
7544
  }
7820
7545
  }
7821
7546
  );
7822
- 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(
7823
7548
  new Option(
7824
7549
  "--author-identity <mode>",
7825
7550
  "author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
@@ -7844,9 +7569,9 @@ program.command("ci").argument("[path]", "path to the project to analyze").addOp
7844
7569
  "--main-branches <names>",
7845
7570
  "comma-separated default branch candidates for auto baseline resolution (for example: main,master)"
7846
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(
7847
- "--max-quality-delta <value>",
7848
- "maximum allowed normalized quality score regression versus baseline"
7849
- ).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-quality-score <score>", "minimum quality score threshold (0..100)").addOption(
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(
7850
7575
  new Option("--fail-on <level>", "failing severity threshold").choices(["error", "warn"]).default("error")
7851
7576
  ).option("--no-trace", "disable trace embedding in generated snapshot").addOption(
7852
7577
  new Option(
@@ -7871,7 +7596,7 @@ program.command("ci").argument("[path]", "path to the project to analyze").addOp
7871
7596
  ...options.report === void 0 ? {} : { reportPath: options.report },
7872
7597
  ...options.jsonOutput === void 0 ? {} : { jsonOutputPath: options.jsonOutput },
7873
7598
  includeTrace: options.trace,
7874
- riskProfile: options.riskProfile,
7599
+ scoringProfile: options.scoringProfile,
7875
7600
  recentWindowDays: options.recentWindowDays,
7876
7601
  gateConfig
7877
7602
  },