@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/README.md +23 -31
- package/dist/index.js +745 -1020
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
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,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("
|
|
1732
|
-
lines.push(`
|
|
1733
|
-
lines.push(` normalizedScore: ${report.
|
|
1734
|
-
lines.push(` modularity: ${report.
|
|
1735
|
-
lines.push(` changeHygiene: ${report.
|
|
1736
|
-
lines.push(`
|
|
1737
|
-
lines.push(`
|
|
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.
|
|
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.
|
|
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("##
|
|
1827
|
-
lines.push(`-
|
|
1828
|
-
lines.push(`- normalizedScore: \`${report.
|
|
1829
|
-
lines.push(`- modularity: \`${report.
|
|
1830
|
-
lines.push(`- changeHygiene: \`${report.
|
|
1831
|
-
lines.push(`-
|
|
1832
|
-
lines.push(`-
|
|
1833
|
-
|
|
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.
|
|
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.
|
|
1996
|
-
throw new GovernanceConfigurationError("max-
|
|
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.
|
|
2005
|
-
throw new GovernanceConfigurationError("min-
|
|
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.
|
|
2034
|
-
evaluatedGates.push("min-
|
|
2035
|
-
const current = input.current.analysis.
|
|
2036
|
-
if (current < config.
|
|
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-
|
|
2035
|
+
"min-health-score",
|
|
2040
2036
|
"error",
|
|
2041
|
-
`
|
|
2037
|
+
`Health score ${current} is below configured minimum ${config.minHealthScore}.`,
|
|
2042
2038
|
[input.current.analysis.structural.targetPath],
|
|
2043
|
-
[{ kind: "repository_metric", metric: "
|
|
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.
|
|
2069
|
-
evaluatedGates.push("max-
|
|
2070
|
-
requireDiff(input, "max-
|
|
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-
|
|
2069
|
+
throw new GovernanceConfigurationError("max-health-delta requires baseline snapshot");
|
|
2074
2070
|
}
|
|
2075
|
-
const delta = input.current.analysis.
|
|
2076
|
-
if (delta < -config.
|
|
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-
|
|
2075
|
+
"max-health-delta",
|
|
2080
2076
|
"error",
|
|
2081
|
-
`
|
|
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: "
|
|
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
|
|
2546
|
-
import { dirname as dirname2, resolve as
|
|
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
|
-
|
|
2588
|
-
|
|
2589
|
-
normalizedScore: summary.
|
|
2590
|
-
dimensions: summary.
|
|
2591
|
-
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)
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
// ../
|
|
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.
|
|
4889
|
-
changeHygiene: 0.
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
var
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
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
|
-
|
|
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,
|
|
4401
|
+
var createDimensionTrace = (dimension, health, factors) => ({
|
|
4913
4402
|
dimension,
|
|
4914
|
-
normalizedScore: round45(clamp01(
|
|
4915
|
-
score: toPercentage(
|
|
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
|
|
4920
|
-
|
|
4921
|
-
|
|
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 !
|
|
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
|
|
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
|
|
4939
|
-
const
|
|
4940
|
-
const
|
|
4941
|
-
const
|
|
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
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
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: "
|
|
4973
|
-
penalty:
|
|
4482
|
+
factorId: "health.modularity.cycle_density",
|
|
4483
|
+
penalty: cycleDensityPenalty,
|
|
4974
4484
|
rawMetrics: {
|
|
4975
|
-
cycleCount,
|
|
4976
|
-
|
|
4485
|
+
cycleCount: input.structural.metrics.cycleCount,
|
|
4486
|
+
cycleEdgeRatio: round45(cycleEdgeRatio),
|
|
4487
|
+
cycleNodeRatio: round45(cycleNodeRatio)
|
|
4977
4488
|
},
|
|
4978
4489
|
normalizedMetrics: {
|
|
4979
|
-
|
|
4490
|
+
cycleDensityPenalty: round45(cycleDensityPenalty)
|
|
4980
4491
|
},
|
|
4981
|
-
weight: 0.
|
|
4982
|
-
evidence: [{ kind: "repository_metric", metric: "structural.
|
|
4492
|
+
weight: 0.4,
|
|
4493
|
+
evidence: [{ kind: "repository_metric", metric: "structural.cycleEdgeRatio" }]
|
|
4983
4494
|
},
|
|
4984
4495
|
{
|
|
4985
|
-
factorId: "
|
|
4986
|
-
penalty:
|
|
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
|
-
|
|
4503
|
+
fanConcentration: round45(fanConcentration)
|
|
4993
4504
|
},
|
|
4994
|
-
weight: 0.
|
|
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 =
|
|
4999
|
-
modularityFactors
|
|
4533
|
+
const modularityPenalty = dampenForSmallSamples(
|
|
4534
|
+
weightedPenalty(modularityFactors),
|
|
4535
|
+
sourceFileCount,
|
|
4536
|
+
8,
|
|
4537
|
+
0.45
|
|
5000
4538
|
);
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
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
|
-
|
|
5010
|
-
|
|
5011
|
-
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
|
|
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
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
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: "
|
|
5026
|
-
ruleId: "git.
|
|
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: "
|
|
5030
|
-
|
|
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 (
|
|
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: "
|
|
5039
|
-
ruleId: "git.
|
|
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
|
|
5043
|
-
|
|
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 (
|
|
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: "
|
|
5052
|
-
ruleId: "git.
|
|
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: "
|
|
5056
|
-
|
|
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: "
|
|
5075
|
-
penalty:
|
|
4687
|
+
factorId: "health.change_hygiene.churn_concentration",
|
|
4688
|
+
penalty: churnConcentrationPenalty,
|
|
5076
4689
|
rawMetrics: {
|
|
5077
|
-
|
|
4690
|
+
top10PercentFilesChurnShare: round45(top10PercentFilesChurnShare)
|
|
5078
4691
|
},
|
|
5079
4692
|
normalizedMetrics: {
|
|
5080
|
-
|
|
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
|
-
|
|
5092
|
-
|
|
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: "
|
|
5099
|
-
penalty:
|
|
4699
|
+
factorId: "health.change_hygiene.volatility_concentration",
|
|
4700
|
+
penalty: volatilityConcentrationPenalty,
|
|
5100
4701
|
rawMetrics: {
|
|
5101
|
-
|
|
5102
|
-
couplingIntensity: round45(couplingIntensity)
|
|
4702
|
+
top10PercentFilesVolatilityShare: round45(top10PercentFilesVolatilityShare)
|
|
5103
4703
|
},
|
|
5104
4704
|
normalizedMetrics: {
|
|
5105
|
-
|
|
4705
|
+
volatilityConcentrationPenalty: round45(volatilityConcentrationPenalty)
|
|
5106
4706
|
},
|
|
5107
4707
|
weight: 0.3,
|
|
5108
|
-
evidence: [
|
|
4708
|
+
evidence: [
|
|
4709
|
+
{
|
|
4710
|
+
kind: "repository_metric",
|
|
4711
|
+
metric: "evolution.top10PercentFilesVolatilityShare"
|
|
4712
|
+
}
|
|
4713
|
+
]
|
|
5109
4714
|
},
|
|
5110
4715
|
{
|
|
5111
|
-
factorId: "
|
|
5112
|
-
penalty:
|
|
4716
|
+
factorId: "health.change_hygiene.dense_co_change_clusters",
|
|
4717
|
+
penalty: coChangeClusterPenalty,
|
|
5113
4718
|
rawMetrics: {
|
|
5114
|
-
|
|
4719
|
+
denseCoChangePairRatio: round45(denseCoChangePairRatio)
|
|
5115
4720
|
},
|
|
5116
4721
|
normalizedMetrics: {
|
|
5117
|
-
|
|
4722
|
+
coChangeClusterPenalty: round45(coChangeClusterPenalty)
|
|
5118
4723
|
},
|
|
5119
|
-
weight: 0.
|
|
5120
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
4724
|
+
weight: 0.3,
|
|
4725
|
+
evidence: [{ kind: "repository_metric", metric: "evolution.denseCoChangePairRatio" }]
|
|
5121
4726
|
}
|
|
5122
4727
|
];
|
|
5123
|
-
const changeHygienePenalty = input.evolution.available ?
|
|
5124
|
-
const
|
|
5125
|
-
const
|
|
5126
|
-
const
|
|
5127
|
-
const
|
|
5128
|
-
const
|
|
5129
|
-
const
|
|
5130
|
-
const
|
|
5131
|
-
const
|
|
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: "
|
|
5147
|
-
penalty:
|
|
4739
|
+
factorId: "health.test_health.test_file_presence",
|
|
4740
|
+
penalty: testPresencePenalty,
|
|
5148
4741
|
rawMetrics: {
|
|
5149
|
-
|
|
4742
|
+
sourceFiles,
|
|
4743
|
+
testFiles
|
|
5150
4744
|
},
|
|
5151
4745
|
normalizedMetrics: {
|
|
5152
|
-
|
|
4746
|
+
testPresencePenalty: round45(testPresencePenalty)
|
|
5153
4747
|
},
|
|
5154
|
-
weight: 0.
|
|
5155
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
4748
|
+
weight: 0.4,
|
|
4749
|
+
evidence: [{ kind: "repository_metric", metric: "tests.filePresence" }]
|
|
5156
4750
|
},
|
|
5157
4751
|
{
|
|
5158
|
-
factorId: "
|
|
5159
|
-
penalty:
|
|
4752
|
+
factorId: "health.test_health.test_to_source_ratio",
|
|
4753
|
+
penalty: testRatioPenalty,
|
|
5160
4754
|
rawMetrics: {
|
|
5161
|
-
|
|
5162
|
-
typeScriptFilesWithDiagnostics: tsc?.filesWithDiagnostics ?? 0
|
|
4755
|
+
testToSourceRatio: round45(testRatio)
|
|
5163
4756
|
},
|
|
5164
4757
|
normalizedMetrics: {
|
|
5165
|
-
|
|
4758
|
+
testRatioPenalty: round45(testRatioPenalty)
|
|
5166
4759
|
},
|
|
5167
|
-
weight: 0.
|
|
5168
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
4760
|
+
weight: 0.45,
|
|
4761
|
+
evidence: [{ kind: "repository_metric", metric: "tests.testToSourceRatio" }]
|
|
5169
4762
|
},
|
|
5170
4763
|
{
|
|
5171
|
-
factorId: "
|
|
5172
|
-
penalty:
|
|
4764
|
+
factorId: "health.test_health.testing_directory_presence",
|
|
4765
|
+
penalty: testingDirectoryPenalty,
|
|
5173
4766
|
rawMetrics: {
|
|
5174
|
-
|
|
4767
|
+
testingDirectoryPresent: testingDirectoryPresent ? 1 : 0
|
|
5175
4768
|
},
|
|
5176
4769
|
normalizedMetrics: {
|
|
5177
|
-
|
|
4770
|
+
testingDirectoryPenalty: round45(testingDirectoryPenalty)
|
|
5178
4771
|
},
|
|
5179
|
-
weight: 0.
|
|
5180
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
4772
|
+
weight: 0.15,
|
|
4773
|
+
evidence: [{ kind: "repository_metric", metric: "tests.directoryPresence" }]
|
|
5181
4774
|
}
|
|
5182
4775
|
];
|
|
5183
|
-
const
|
|
5184
|
-
|
|
4776
|
+
const testHealthPenalty = dampenForSmallSamples(
|
|
4777
|
+
weightedPenalty(testHealthFactors),
|
|
4778
|
+
sourceFiles,
|
|
4779
|
+
10,
|
|
4780
|
+
0.3
|
|
5185
4781
|
);
|
|
5186
|
-
if (
|
|
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: "
|
|
5192
|
-
ruleId:
|
|
5193
|
-
|
|
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:
|
|
5196
|
-
severity: "error",
|
|
5197
|
-
|
|
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 (
|
|
4799
|
+
if (sourceFiles > 0 && testRatio < 0.12) {
|
|
5201
4800
|
pushIssue(issues, {
|
|
5202
|
-
id: "
|
|
5203
|
-
ruleId: "
|
|
5204
|
-
|
|
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:
|
|
5207
|
-
|
|
5208
|
-
|
|
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
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
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
|
-
|
|
5223
|
-
|
|
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
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
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
|
-
|
|
5235
|
-
|
|
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
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
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
|
-
|
|
5248
|
-
|
|
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
|
-
|
|
5251
|
-
|
|
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
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
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
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
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
|
-
|
|
5337
|
-
|
|
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: "
|
|
5341
|
-
penalty:
|
|
5089
|
+
factorId: "health.ownership.missing_git_history",
|
|
5090
|
+
penalty: ownershipDistributionPenalty,
|
|
5342
5091
|
rawMetrics: {
|
|
5343
|
-
|
|
5344
|
-
branchCoverage: coverageSignals?.branchCoverage ?? null,
|
|
5345
|
-
functionCoverage: coverageSignals?.functionCoverage ?? null,
|
|
5346
|
-
statementCoverage: coverageSignals?.statementCoverage ?? null
|
|
5092
|
+
gitHistoryAvailable: 0
|
|
5347
5093
|
},
|
|
5348
5094
|
normalizedMetrics: {
|
|
5349
|
-
|
|
5350
|
-
coveragePenalty: round45(coveragePenalty)
|
|
5095
|
+
ownershipDistributionPenalty: round45(ownershipDistributionPenalty)
|
|
5351
5096
|
},
|
|
5352
|
-
weight:
|
|
5353
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
5097
|
+
weight: 1,
|
|
5098
|
+
evidence: [{ kind: "repository_metric", metric: "evolution.available" }]
|
|
5354
5099
|
}
|
|
5355
5100
|
];
|
|
5356
|
-
const
|
|
5357
|
-
|
|
5358
|
-
);
|
|
5359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5112
|
+
healthScore: toPercentage(normalizedScore),
|
|
5394
5113
|
normalizedScore: round45(normalizedScore),
|
|
5395
5114
|
dimensions: {
|
|
5396
|
-
modularity: toPercentage(
|
|
5397
|
-
changeHygiene: toPercentage(
|
|
5398
|
-
|
|
5399
|
-
|
|
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:
|
|
5122
|
+
schemaVersion: HEALTH_TRACE_VERSION,
|
|
5406
5123
|
dimensions: [
|
|
5407
|
-
createDimensionTrace("modularity",
|
|
5408
|
-
createDimensionTrace("changeHygiene",
|
|
5409
|
-
createDimensionTrace("
|
|
5410
|
-
createDimensionTrace(
|
|
5411
|
-
|
|
5412
|
-
|
|
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) =>
|
|
6657
|
-
var
|
|
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
|
|
6670
|
-
|
|
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.
|
|
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
|
|
6575
|
+
const health = computeRepositoryHealthSummary({
|
|
6853
6576
|
structural: analysisInputs.structural,
|
|
6854
6577
|
evolution: analysisInputs.evolution,
|
|
6855
|
-
|
|
6578
|
+
...healthConfig === void 0 ? {} : { config: healthConfig }
|
|
6856
6579
|
});
|
|
6857
6580
|
logger.info(
|
|
6858
|
-
`analysis completed (riskScore=${risk.riskScore},
|
|
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
|
-
|
|
6588
|
+
health
|
|
6866
6589
|
};
|
|
6867
6590
|
};
|
|
6868
6591
|
|
|
6869
6592
|
// src/application/run-check-command.ts
|
|
6870
|
-
import { readFile as
|
|
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.
|
|
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
|
-
|
|
6621
|
+
health: computeRepositoryHealthSummary({
|
|
6898
6622
|
structural: analysisInputs.structural,
|
|
6899
6623
|
evolution: analysisInputs.evolution,
|
|
6900
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
6993
|
-
import { relative as
|
|
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 =
|
|
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.
|
|
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 =
|
|
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 :
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
6948
|
+
health: computeRepositoryHealthSummary({
|
|
7224
6949
|
structural: analysisInputs.structural,
|
|
7225
6950
|
evolution: analysisInputs.evolution,
|
|
7226
|
-
|
|
6951
|
+
...healthConfig === void 0 ? {} : { config: healthConfig }
|
|
7227
6952
|
})
|
|
7228
6953
|
};
|
|
7229
6954
|
logger.info(
|
|
7230
|
-
`explanation completed (riskScore=${summary.risk.riskScore},
|
|
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 =
|
|
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(`
|
|
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(`-
|
|
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(`
|
|
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(`-
|
|
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
|
|
7367
|
-
"--
|
|
7368
|
-
"
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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(
|
|
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-
|
|
7777
|
-
"maximum allowed normalized
|
|
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-
|
|
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
|
-
|
|
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(
|
|
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-
|
|
7848
|
-
"maximum allowed normalized
|
|
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-
|
|
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
|
-
|
|
7599
|
+
scoringProfile: options.scoringProfile,
|
|
7875
7600
|
recentWindowDays: options.recentWindowDays,
|
|
7876
7601
|
gateConfig
|
|
7877
7602
|
},
|