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