@getcodesentinel/codesentinel 1.13.0 → 1.15.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 +33 -1
- package/dist/index.js +755 -123
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1656,6 +1656,7 @@ var createReport = (snapshot, diff) => {
|
|
|
1656
1656
|
confidence: repositoryConfidence(snapshot),
|
|
1657
1657
|
dimensionScores: repositoryDimensionScores(snapshot)
|
|
1658
1658
|
},
|
|
1659
|
+
quality: snapshot.analysis.quality,
|
|
1659
1660
|
hotspots: hotspotItems(snapshot),
|
|
1660
1661
|
structural: {
|
|
1661
1662
|
cycleCount: snapshot.analysis.structural.metrics.cycleCount,
|
|
@@ -1727,6 +1728,22 @@ var renderTextReport = (report) => {
|
|
|
1727
1728
|
lines.push(` external: ${report.repository.dimensionScores.external ?? "n/a"}`);
|
|
1728
1729
|
lines.push(` interactions: ${report.repository.dimensionScores.interactions ?? "n/a"}`);
|
|
1729
1730
|
lines.push("");
|
|
1731
|
+
lines.push("Quality Summary");
|
|
1732
|
+
lines.push(` qualityScore: ${report.quality.qualityScore}`);
|
|
1733
|
+
lines.push(` normalizedScore: ${report.quality.normalizedScore}`);
|
|
1734
|
+
lines.push(` modularity: ${report.quality.dimensions.modularity}`);
|
|
1735
|
+
lines.push(` changeHygiene: ${report.quality.dimensions.changeHygiene}`);
|
|
1736
|
+
lines.push(` testHealth: ${report.quality.dimensions.testHealth}`);
|
|
1737
|
+
lines.push(" topIssues:");
|
|
1738
|
+
for (const issue of report.quality.topIssues.slice(0, 5)) {
|
|
1739
|
+
lines.push(
|
|
1740
|
+
` - [${issue.severity}] (${issue.dimension}) ${issue.id} @ ${issue.target}: ${issue.message}`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
if (report.quality.topIssues.length === 0) {
|
|
1744
|
+
lines.push(" - none");
|
|
1745
|
+
}
|
|
1746
|
+
lines.push("");
|
|
1730
1747
|
lines.push("Top Hotspots");
|
|
1731
1748
|
for (const hotspot of report.hotspots) {
|
|
1732
1749
|
lines.push(` - ${hotspot.target} | score=${hotspot.score}`);
|
|
@@ -1736,8 +1753,7 @@ var renderTextReport = (report) => {
|
|
|
1736
1753
|
);
|
|
1737
1754
|
lines.push(` evidence: ${factor.evidence}`);
|
|
1738
1755
|
}
|
|
1739
|
-
lines.push(` actions: ${hotspot.suggestedActions.join(" | ") || "none"}`);
|
|
1740
|
-
lines.push(` levers: ${hotspot.biggestLevers.join(" | ") || "none"}`);
|
|
1756
|
+
lines.push(` priority actions: ${hotspot.suggestedActions.join(" | ") || "none"}`);
|
|
1741
1757
|
}
|
|
1742
1758
|
lines.push("");
|
|
1743
1759
|
lines.push("Structural Observations");
|
|
@@ -1803,6 +1819,23 @@ var renderMarkdownReport = (report) => {
|
|
|
1803
1819
|
lines.push(`- external: \`${report.repository.dimensionScores.external ?? "n/a"}\``);
|
|
1804
1820
|
lines.push(`- interactions: \`${report.repository.dimensionScores.interactions ?? "n/a"}\``);
|
|
1805
1821
|
lines.push("");
|
|
1822
|
+
lines.push("## Quality Summary");
|
|
1823
|
+
lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
|
|
1824
|
+
lines.push(`- normalizedScore: \`${report.quality.normalizedScore}\``);
|
|
1825
|
+
lines.push(`- modularity: \`${report.quality.dimensions.modularity}\``);
|
|
1826
|
+
lines.push(`- changeHygiene: \`${report.quality.dimensions.changeHygiene}\``);
|
|
1827
|
+
lines.push(`- testHealth: \`${report.quality.dimensions.testHealth}\``);
|
|
1828
|
+
if (report.quality.topIssues.length === 0) {
|
|
1829
|
+
lines.push("- top issues: none");
|
|
1830
|
+
} else {
|
|
1831
|
+
lines.push("- top issues:");
|
|
1832
|
+
for (const issue of report.quality.topIssues.slice(0, 5)) {
|
|
1833
|
+
lines.push(
|
|
1834
|
+
` - [${issue.severity}] \`${issue.id}\` (\`${issue.dimension}\`) @ \`${issue.target}\`: ${issue.message}`
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
lines.push("");
|
|
1806
1839
|
lines.push("## Top Hotspots");
|
|
1807
1840
|
for (const hotspot of report.hotspots) {
|
|
1808
1841
|
lines.push(`- **${hotspot.target}** (score: \`${hotspot.score}\`)`);
|
|
@@ -1813,8 +1846,7 @@ var renderMarkdownReport = (report) => {
|
|
|
1813
1846
|
);
|
|
1814
1847
|
lines.push(` - evidence: \`${factor.evidence}\``);
|
|
1815
1848
|
}
|
|
1816
|
-
lines.push(` -
|
|
1817
|
-
lines.push(` - Biggest levers: ${hotspot.biggestLevers.join(" | ") || "none"}`);
|
|
1849
|
+
lines.push(` - Priority actions: ${hotspot.suggestedActions.join(" | ") || "none"}`);
|
|
1818
1850
|
}
|
|
1819
1851
|
lines.push("");
|
|
1820
1852
|
lines.push("## Structural Observations");
|
|
@@ -2461,6 +2493,7 @@ var resolveAutoBaselineRef = async (input) => {
|
|
|
2461
2493
|
|
|
2462
2494
|
// src/index.ts
|
|
2463
2495
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2496
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
2464
2497
|
import { dirname as dirname2, resolve as resolve5 } from "path";
|
|
2465
2498
|
import { fileURLToPath } from "url";
|
|
2466
2499
|
|
|
@@ -2501,6 +2534,12 @@ var createSummaryShape = (summary) => ({
|
|
|
2501
2534
|
})),
|
|
2502
2535
|
fragileClusterCount: summary.risk.fragileClusters.length,
|
|
2503
2536
|
dependencyAmplificationZoneCount: summary.risk.dependencyAmplificationZones.length
|
|
2537
|
+
},
|
|
2538
|
+
quality: {
|
|
2539
|
+
qualityScore: summary.quality.qualityScore,
|
|
2540
|
+
normalizedScore: summary.quality.normalizedScore,
|
|
2541
|
+
dimensions: summary.quality.dimensions,
|
|
2542
|
+
topIssues: summary.quality.topIssues.slice(0, 5)
|
|
2504
2543
|
}
|
|
2505
2544
|
});
|
|
2506
2545
|
var formatAnalyzeOutput = (summary, mode) => mode === "json" ? JSON.stringify(summary, null, 2) : JSON.stringify(createSummaryShape(summary), null, 2);
|
|
@@ -2544,6 +2583,7 @@ var formatFactorLabel = (factorId) => factorLabelById2[factorId] ?? factorId;
|
|
|
2544
2583
|
var formatNumber = (value) => value === null || value === void 0 ? "n/a" : `${value}`;
|
|
2545
2584
|
var formatDimension = (value) => value === null ? "n/a" : `${value}`;
|
|
2546
2585
|
var formatFactorSummary = (factor) => `${formatFactorLabel(factor.factorId)} (+${factor.contribution}, confidence=${factor.confidence})`;
|
|
2586
|
+
var formatFactorContribution = (factor) => `${formatFactorLabel(factor.factorId)}=${factor.contribution}`;
|
|
2547
2587
|
var formatFactorEvidence = (factor) => {
|
|
2548
2588
|
if (factor.factorId === "repository.structural") {
|
|
2549
2589
|
return `structural dimension=${formatNumber(factor.rawMetrics["structuralDimension"])}`;
|
|
@@ -2676,20 +2716,15 @@ var renderText = (payload) => {
|
|
|
2676
2716
|
lines.push("");
|
|
2677
2717
|
lines.push("explanation:");
|
|
2678
2718
|
lines.push(
|
|
2679
|
-
`
|
|
2680
|
-
);
|
|
2681
|
-
lines.push(
|
|
2682
|
-
` what specifically contributed: ${repositoryTopFactors.map((factor) => `${formatFactorLabel(factor.factorId)}=${factor.contribution}`).join(", ") || "insufficient data"}`
|
|
2683
|
-
);
|
|
2684
|
-
lines.push(
|
|
2685
|
-
` dominant factors: ${repositoryTopFactors.map((factor) => formatFactorLabel(factor.factorId)).join(", ") || "insufficient data"}`
|
|
2719
|
+
` key drivers: ${repositoryTopFactors.map(formatFactorSummary).join("; ") || "insufficient data"}`
|
|
2686
2720
|
);
|
|
2687
2721
|
lines.push(
|
|
2688
|
-
`
|
|
2722
|
+
` contributions: ${repositoryTopFactors.map(formatFactorContribution).join(", ") || "insufficient data"}`
|
|
2689
2723
|
);
|
|
2690
2724
|
lines.push(
|
|
2691
|
-
`
|
|
2725
|
+
` interaction effects: ${compositeFactors.map((factor) => `${formatFactorLabel(factor.factorId)} [${formatFactorEvidence(factor)}]`).join("; ") || "none"}`
|
|
2692
2726
|
);
|
|
2727
|
+
lines.push(` priority actions: ${buildRepositoryActions(payload, repositoryTarget).join(" ")}`);
|
|
2693
2728
|
lines.push("");
|
|
2694
2729
|
for (const target of payload.selectedTargets) {
|
|
2695
2730
|
lines.push(renderTargetText(target));
|
|
@@ -2717,25 +2752,19 @@ var renderMarkdown = (payload) => {
|
|
|
2717
2752
|
lines.push("");
|
|
2718
2753
|
lines.push(`## Summary`);
|
|
2719
2754
|
lines.push(
|
|
2720
|
-
`-
|
|
2755
|
+
`- key drivers: ${repositoryTopFactors.map(formatFactorSummary).join("; ") || "insufficient data"}`
|
|
2721
2756
|
);
|
|
2722
2757
|
lines.push(
|
|
2723
|
-
`-
|
|
2758
|
+
`- contributions: ${repositoryTopFactors.map(formatFactorContribution).join(", ") || "insufficient data"}`
|
|
2724
2759
|
);
|
|
2725
2760
|
lines.push(
|
|
2726
|
-
`-
|
|
2727
|
-
);
|
|
2728
|
-
lines.push(
|
|
2729
|
-
`- intersected signals: ${compositeFactors.map((factor) => `${formatFactorLabel(factor.factorId)} [${formatFactorEvidence(factor)}]`).join("; ") || "none"}`
|
|
2730
|
-
);
|
|
2731
|
-
lines.push(
|
|
2732
|
-
`- what could reduce risk most: ${buildRepositoryActions(payload, repositoryTarget).join(" ")}`
|
|
2761
|
+
`- interaction effects: ${compositeFactors.map((factor) => `${formatFactorLabel(factor.factorId)} [${formatFactorEvidence(factor)}]`).join("; ") || "none"}`
|
|
2733
2762
|
);
|
|
2763
|
+
lines.push(`- priority actions: ${buildRepositoryActions(payload, repositoryTarget).join(" ")}`);
|
|
2734
2764
|
lines.push("");
|
|
2735
2765
|
for (const target of payload.selectedTargets) {
|
|
2736
2766
|
lines.push(`## ${target.targetType}: \`${target.targetId}\``);
|
|
2737
2767
|
lines.push(`- score: \`${target.totalScore}\` (\`${target.normalizedScore}\`)`);
|
|
2738
|
-
lines.push(`- dominantFactors: \`${target.dominantFactors.join(", ")}\``);
|
|
2739
2768
|
lines.push(`- Top factors:`);
|
|
2740
2769
|
for (const factor of [...target.factors].sort(sortFactorByContribution).slice(0, 5)) {
|
|
2741
2770
|
lines.push(
|
|
@@ -3187,7 +3216,8 @@ var checkForCliUpdates = async (input) => {
|
|
|
3187
3216
|
};
|
|
3188
3217
|
|
|
3189
3218
|
// src/application/run-analyze-command.ts
|
|
3190
|
-
import {
|
|
3219
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
3220
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
3191
3221
|
|
|
3192
3222
|
// ../code-graph/dist/index.js
|
|
3193
3223
|
import { extname, isAbsolute, relative, resolve as resolve2 } from "path";
|
|
@@ -4235,6 +4265,208 @@ var analyzeRepositoryEvolutionFromGit = (input, onProgress) => {
|
|
|
4235
4265
|
return analyzeRepositoryEvolution(input, historyProvider, onProgress);
|
|
4236
4266
|
};
|
|
4237
4267
|
|
|
4268
|
+
// ../quality-engine/dist/index.js
|
|
4269
|
+
var clamp01 = (value) => {
|
|
4270
|
+
if (!Number.isFinite(value)) {
|
|
4271
|
+
return 0;
|
|
4272
|
+
}
|
|
4273
|
+
if (value <= 0) {
|
|
4274
|
+
return 0;
|
|
4275
|
+
}
|
|
4276
|
+
if (value >= 1) {
|
|
4277
|
+
return 1;
|
|
4278
|
+
}
|
|
4279
|
+
return value;
|
|
4280
|
+
};
|
|
4281
|
+
var round45 = (value) => Number(value.toFixed(4));
|
|
4282
|
+
var average = (values) => {
|
|
4283
|
+
if (values.length === 0) {
|
|
4284
|
+
return 0;
|
|
4285
|
+
}
|
|
4286
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
4287
|
+
return total / values.length;
|
|
4288
|
+
};
|
|
4289
|
+
var concentration = (rawValues) => {
|
|
4290
|
+
const values = rawValues.filter((value) => value > 0);
|
|
4291
|
+
const count = values.length;
|
|
4292
|
+
if (count <= 1) {
|
|
4293
|
+
return 0;
|
|
4294
|
+
}
|
|
4295
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
4296
|
+
if (total <= 0) {
|
|
4297
|
+
return 0;
|
|
4298
|
+
}
|
|
4299
|
+
const hhi = values.reduce((sum, value) => {
|
|
4300
|
+
const share = value / total;
|
|
4301
|
+
return sum + share * share;
|
|
4302
|
+
}, 0);
|
|
4303
|
+
const minHhi = 1 / count;
|
|
4304
|
+
const normalized = (hhi - minHhi) / (1 - minHhi);
|
|
4305
|
+
return clamp01(normalized);
|
|
4306
|
+
};
|
|
4307
|
+
var DIMENSION_WEIGHTS = {
|
|
4308
|
+
modularity: 0.45,
|
|
4309
|
+
changeHygiene: 0.35,
|
|
4310
|
+
testHealth: 0.2
|
|
4311
|
+
};
|
|
4312
|
+
var TODO_FIXME_MAX_IMPACT = 0.08;
|
|
4313
|
+
var toPercentage = (normalizedQuality) => round45(clamp01(normalizedQuality) * 100);
|
|
4314
|
+
var filePaths = (structural) => structural.files.map((file) => file.relativePath);
|
|
4315
|
+
var isTestPath = (path) => {
|
|
4316
|
+
const normalized = path.toLowerCase();
|
|
4317
|
+
return normalized.includes("/__tests__/") || normalized.includes("\\__tests__\\") || normalized.includes(".test.") || normalized.includes(".spec.");
|
|
4318
|
+
};
|
|
4319
|
+
var isSourcePath = (path) => {
|
|
4320
|
+
if (path.endsWith(".d.ts")) {
|
|
4321
|
+
return false;
|
|
4322
|
+
}
|
|
4323
|
+
return !isTestPath(path);
|
|
4324
|
+
};
|
|
4325
|
+
var pushIssue = (issues, issue) => {
|
|
4326
|
+
issues.push({
|
|
4327
|
+
...issue,
|
|
4328
|
+
severity: issue.severity ?? "warn"
|
|
4329
|
+
});
|
|
4330
|
+
};
|
|
4331
|
+
var computeRepositoryQualitySummary = (input) => {
|
|
4332
|
+
const issues = [];
|
|
4333
|
+
const sourceFileSet = new Set(input.structural.files.map((file) => file.relativePath));
|
|
4334
|
+
const cycleCount = input.structural.metrics.cycleCount;
|
|
4335
|
+
const cycleSizeAverage = input.structural.cycles.length === 0 ? 0 : average(input.structural.cycles.map((cycle) => cycle.nodes.length));
|
|
4336
|
+
const cyclePenalty = clamp01(cycleCount / 6) * 0.7 + clamp01((cycleSizeAverage - 2) / 8) * 0.3;
|
|
4337
|
+
if (cycleCount > 0) {
|
|
4338
|
+
pushIssue(issues, {
|
|
4339
|
+
id: "quality.modularity.structural_cycles",
|
|
4340
|
+
dimension: "modularity",
|
|
4341
|
+
target: input.structural.cycles[0]?.nodes.slice().sort((a, b) => a.localeCompare(b)).join(" -> ") ?? input.structural.targetPath,
|
|
4342
|
+
message: `${cycleCount} structural cycle(s) increase coupling and refactor cost.`,
|
|
4343
|
+
severity: cycleCount >= 3 ? "error" : "warn",
|
|
4344
|
+
impact: round45(cyclePenalty * 0.6)
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
const fanInConcentration = concentration(input.structural.files.map((file) => file.fanIn));
|
|
4348
|
+
const fanOutConcentration = concentration(input.structural.files.map((file) => file.fanOut));
|
|
4349
|
+
const centralityConcentration = average([fanInConcentration, fanOutConcentration]);
|
|
4350
|
+
if (centralityConcentration >= 0.5) {
|
|
4351
|
+
const hottest = [...input.structural.files].map((file) => ({
|
|
4352
|
+
path: file.relativePath,
|
|
4353
|
+
pressure: file.fanIn + file.fanOut
|
|
4354
|
+
})).sort((a, b) => b.pressure - a.pressure || a.path.localeCompare(b.path))[0];
|
|
4355
|
+
pushIssue(issues, {
|
|
4356
|
+
id: "quality.modularity.centrality_concentration",
|
|
4357
|
+
dimension: "modularity",
|
|
4358
|
+
target: hottest?.path ?? input.structural.targetPath,
|
|
4359
|
+
message: "Fan-in/fan-out pressure is concentrated in a small set of files.",
|
|
4360
|
+
impact: round45(centralityConcentration * 0.5)
|
|
4361
|
+
});
|
|
4362
|
+
}
|
|
4363
|
+
let churnConcentration = 0;
|
|
4364
|
+
let volatilityConcentration = 0;
|
|
4365
|
+
let couplingDensity = 0;
|
|
4366
|
+
let couplingIntensity = 0;
|
|
4367
|
+
if (input.evolution.available) {
|
|
4368
|
+
const evolutionSourceFiles = input.evolution.files.filter(
|
|
4369
|
+
(file) => sourceFileSet.has(file.filePath)
|
|
4370
|
+
);
|
|
4371
|
+
churnConcentration = concentration(evolutionSourceFiles.map((file) => file.churnTotal));
|
|
4372
|
+
volatilityConcentration = concentration(
|
|
4373
|
+
evolutionSourceFiles.map((file) => file.recentVolatility)
|
|
4374
|
+
);
|
|
4375
|
+
const fileCount = Math.max(1, evolutionSourceFiles.length);
|
|
4376
|
+
const maxPairs = fileCount * (fileCount - 1) / 2;
|
|
4377
|
+
const sourcePairs = input.evolution.coupling.pairs.filter(
|
|
4378
|
+
(pair) => sourceFileSet.has(pair.fileA) && sourceFileSet.has(pair.fileB)
|
|
4379
|
+
);
|
|
4380
|
+
couplingDensity = maxPairs <= 0 ? 0 : clamp01(sourcePairs.length / maxPairs);
|
|
4381
|
+
couplingIntensity = average(sourcePairs.map((pair) => pair.couplingScore));
|
|
4382
|
+
if (churnConcentration >= 0.45) {
|
|
4383
|
+
const mostChurn = [...evolutionSourceFiles].sort(
|
|
4384
|
+
(a, b) => b.churnTotal - a.churnTotal || a.filePath.localeCompare(b.filePath)
|
|
4385
|
+
)[0];
|
|
4386
|
+
pushIssue(issues, {
|
|
4387
|
+
id: "quality.change_hygiene.churn_concentration",
|
|
4388
|
+
dimension: "changeHygiene",
|
|
4389
|
+
target: mostChurn?.filePath ?? input.structural.targetPath,
|
|
4390
|
+
message: "Churn is concentrated in a narrow part of the codebase.",
|
|
4391
|
+
impact: round45(churnConcentration * 0.45)
|
|
4392
|
+
});
|
|
4393
|
+
}
|
|
4394
|
+
if (volatilityConcentration >= 0.45) {
|
|
4395
|
+
const volatileFile = [...evolutionSourceFiles].sort(
|
|
4396
|
+
(a, b) => b.recentVolatility - a.recentVolatility || a.filePath.localeCompare(b.filePath)
|
|
4397
|
+
)[0];
|
|
4398
|
+
pushIssue(issues, {
|
|
4399
|
+
id: "quality.change_hygiene.volatility_concentration",
|
|
4400
|
+
dimension: "changeHygiene",
|
|
4401
|
+
target: volatileFile?.filePath ?? input.structural.targetPath,
|
|
4402
|
+
message: "Recent volatility is concentrated in files that change frequently.",
|
|
4403
|
+
impact: round45(volatilityConcentration * 0.4)
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
if (couplingDensity >= 0.35 || couplingIntensity >= 0.45) {
|
|
4407
|
+
const strongestPair = [...sourcePairs].sort(
|
|
4408
|
+
(a, b) => b.couplingScore - a.couplingScore || `${a.fileA}|${a.fileB}`.localeCompare(`${b.fileA}|${b.fileB}`)
|
|
4409
|
+
)[0];
|
|
4410
|
+
pushIssue(issues, {
|
|
4411
|
+
id: "quality.change_hygiene.coupling_density",
|
|
4412
|
+
dimension: "changeHygiene",
|
|
4413
|
+
target: strongestPair === void 0 ? input.structural.targetPath : `${strongestPair.fileA}<->${strongestPair.fileB}`,
|
|
4414
|
+
message: "Co-change relationships are dense, increasing coordination overhead.",
|
|
4415
|
+
impact: round45(average([couplingDensity, couplingIntensity]) * 0.35)
|
|
4416
|
+
});
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
const modularityPenalty = clamp01(cyclePenalty * 0.55 + centralityConcentration * 0.45);
|
|
4420
|
+
const changeHygienePenalty = input.evolution.available ? clamp01(
|
|
4421
|
+
churnConcentration * 0.4 + volatilityConcentration * 0.35 + couplingDensity * 0.15 + couplingIntensity * 0.1
|
|
4422
|
+
) : 0.25;
|
|
4423
|
+
const paths = filePaths(input.structural);
|
|
4424
|
+
const testFiles = paths.filter((path) => isTestPath(path)).length;
|
|
4425
|
+
const sourceFiles = paths.filter((path) => isSourcePath(path)).length;
|
|
4426
|
+
const testRatio = sourceFiles <= 0 ? 1 : testFiles / sourceFiles;
|
|
4427
|
+
const testPresencePenalty = sourceFiles <= 0 ? 0 : 1 - clamp01(testRatio / 0.3);
|
|
4428
|
+
if (sourceFiles > 0 && testRatio < 0.2) {
|
|
4429
|
+
pushIssue(issues, {
|
|
4430
|
+
id: "quality.test_health.low_test_presence",
|
|
4431
|
+
dimension: "testHealth",
|
|
4432
|
+
target: input.structural.targetPath,
|
|
4433
|
+
message: `Detected ${testFiles} test file(s) for ${sourceFiles} source file(s).`,
|
|
4434
|
+
severity: testRatio === 0 ? "error" : "warn",
|
|
4435
|
+
impact: round45(testPresencePenalty * 0.5)
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
4438
|
+
const todoFixmeCount = Math.max(0, input.todoFixmeCount ?? 0);
|
|
4439
|
+
const todoFixmePenalty = clamp01(todoFixmeCount / 120) * TODO_FIXME_MAX_IMPACT;
|
|
4440
|
+
if (todoFixmeCount > 0) {
|
|
4441
|
+
pushIssue(issues, {
|
|
4442
|
+
id: "quality.change_hygiene.todo_fixme_load",
|
|
4443
|
+
dimension: "changeHygiene",
|
|
4444
|
+
target: input.structural.targetPath,
|
|
4445
|
+
message: `Found ${todoFixmeCount} TODO/FIXME marker(s); cleanup debt is accumulating.`,
|
|
4446
|
+
impact: round45(todoFixmePenalty * 0.2)
|
|
4447
|
+
});
|
|
4448
|
+
}
|
|
4449
|
+
const modularityQuality = clamp01(1 - modularityPenalty);
|
|
4450
|
+
const changeHygieneQuality = clamp01(1 - clamp01(changeHygienePenalty + todoFixmePenalty));
|
|
4451
|
+
const testHealthQuality = clamp01(1 - testPresencePenalty);
|
|
4452
|
+
const normalizedScore = clamp01(
|
|
4453
|
+
modularityQuality * DIMENSION_WEIGHTS.modularity + changeHygieneQuality * DIMENSION_WEIGHTS.changeHygiene + testHealthQuality * DIMENSION_WEIGHTS.testHealth
|
|
4454
|
+
);
|
|
4455
|
+
const topIssues = [...issues].sort(
|
|
4456
|
+
(a, b) => b.impact - a.impact || a.id.localeCompare(b.id) || a.target.localeCompare(b.target)
|
|
4457
|
+
).slice(0, 8).map(({ impact: _impact, ...issue }) => issue);
|
|
4458
|
+
return {
|
|
4459
|
+
qualityScore: toPercentage(normalizedScore),
|
|
4460
|
+
normalizedScore: round45(normalizedScore),
|
|
4461
|
+
dimensions: {
|
|
4462
|
+
modularity: toPercentage(modularityQuality),
|
|
4463
|
+
changeHygiene: toPercentage(changeHygieneQuality),
|
|
4464
|
+
testHealth: toPercentage(testHealthQuality)
|
|
4465
|
+
},
|
|
4466
|
+
topIssues
|
|
4467
|
+
};
|
|
4468
|
+
};
|
|
4469
|
+
|
|
4238
4470
|
// ../risk-engine/dist/index.js
|
|
4239
4471
|
var DEFAULT_RISK_ENGINE_CONFIG = {
|
|
4240
4472
|
// Base dimensional influence. Risk is never dominated by a single dimension by default.
|
|
@@ -4320,8 +4552,8 @@ var DEFAULT_RISK_ENGINE_CONFIG = {
|
|
|
4320
4552
|
}
|
|
4321
4553
|
};
|
|
4322
4554
|
var toUnitInterval = (value) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
4323
|
-
var
|
|
4324
|
-
var
|
|
4555
|
+
var round46 = (value) => Number(value.toFixed(4));
|
|
4556
|
+
var average2 = (values) => {
|
|
4325
4557
|
if (values.length === 0) {
|
|
4326
4558
|
return 0;
|
|
4327
4559
|
}
|
|
@@ -4429,14 +4661,14 @@ var computeAggregatorAttenuation = (input) => {
|
|
|
4429
4661
|
const fanOutSignal = toUnitInterval((fanOut - config.minFanOut) / Math.max(1, config.minFanOut));
|
|
4430
4662
|
const lowChurnPerCommitSignal = 1 - toUnitInterval(churnPerCommit / config.maxChurnPerCommit);
|
|
4431
4663
|
const lowChurnPerDependencySignal = 1 - toUnitInterval(churnPerDependency / config.maxChurnPerDependency);
|
|
4432
|
-
const attenuationConfidence =
|
|
4664
|
+
const attenuationConfidence = average2([
|
|
4433
4665
|
fanInSignal,
|
|
4434
4666
|
fanOutSignal,
|
|
4435
4667
|
lowChurnPerCommitSignal,
|
|
4436
4668
|
lowChurnPerDependencySignal
|
|
4437
4669
|
]);
|
|
4438
4670
|
const reduction = toUnitInterval(config.maxStructuralReduction) * attenuationConfidence;
|
|
4439
|
-
return
|
|
4671
|
+
return round46(toUnitInterval(1 - reduction));
|
|
4440
4672
|
};
|
|
4441
4673
|
var dependencySignalWeights = {
|
|
4442
4674
|
single_maintainer: 0.3,
|
|
@@ -4466,12 +4698,12 @@ var computeDependencySignalScore = (ownSignals, inheritedSignals, inheritedSigna
|
|
|
4466
4698
|
}
|
|
4467
4699
|
return toUnitInterval(weightedTotal / maxWeightedTotal);
|
|
4468
4700
|
};
|
|
4469
|
-
var clampConfidence = (value) =>
|
|
4701
|
+
var clampConfidence = (value) => round46(toUnitInterval(value));
|
|
4470
4702
|
var computeExternalMetadataConfidence = (external) => {
|
|
4471
4703
|
if (!external.available) {
|
|
4472
4704
|
return 0;
|
|
4473
4705
|
}
|
|
4474
|
-
return
|
|
4706
|
+
return round46(toUnitInterval(0.35 + external.metrics.metadataCoverage * 0.65));
|
|
4475
4707
|
};
|
|
4476
4708
|
var computeEvolutionHistoryConfidence = (structural, evolution, evolutionByFile) => {
|
|
4477
4709
|
if (!evolution.available) {
|
|
@@ -4488,7 +4720,7 @@ var computeEvolutionHistoryConfidence = (structural, evolution, evolutionByFile)
|
|
|
4488
4720
|
}
|
|
4489
4721
|
}
|
|
4490
4722
|
const coverage = coveredFiles / totalFiles;
|
|
4491
|
-
return
|
|
4723
|
+
return round46(toUnitInterval(0.3 + coverage * 0.7));
|
|
4492
4724
|
};
|
|
4493
4725
|
var buildFactorTraces = (totalScore, inputs) => {
|
|
4494
4726
|
const positiveInputs = inputs.filter((input) => input.strength > 0);
|
|
@@ -4526,7 +4758,7 @@ var buildFactorTraces = (totalScore, inputs) => {
|
|
|
4526
4758
|
continue;
|
|
4527
4759
|
}
|
|
4528
4760
|
if (index === scored.length - 1) {
|
|
4529
|
-
const remaining =
|
|
4761
|
+
const remaining = round46(totalScore - distributed);
|
|
4530
4762
|
traces[traceIndex] = {
|
|
4531
4763
|
...existing,
|
|
4532
4764
|
contribution: Math.max(0, remaining)
|
|
@@ -4534,7 +4766,7 @@ var buildFactorTraces = (totalScore, inputs) => {
|
|
|
4534
4766
|
distributed += Math.max(0, remaining);
|
|
4535
4767
|
continue;
|
|
4536
4768
|
}
|
|
4537
|
-
const rounded =
|
|
4769
|
+
const rounded = round46(current.contribution);
|
|
4538
4770
|
traces[traceIndex] = {
|
|
4539
4771
|
...existing,
|
|
4540
4772
|
contribution: rounded
|
|
@@ -4545,15 +4777,15 @@ var buildFactorTraces = (totalScore, inputs) => {
|
|
|
4545
4777
|
};
|
|
4546
4778
|
var buildReductionLevers = (factors) => factors.filter((factor) => factor.contribution > 0).sort((a, b) => b.contribution - a.contribution || a.factorId.localeCompare(b.factorId)).slice(0, 3).map((factor) => ({
|
|
4547
4779
|
factorId: factor.factorId,
|
|
4548
|
-
estimatedImpact:
|
|
4780
|
+
estimatedImpact: round46(factor.contribution)
|
|
4549
4781
|
}));
|
|
4550
4782
|
var buildTargetTrace = (targetType, targetId, totalScore, normalizedScore, factors) => {
|
|
4551
4783
|
const dominantFactors = [...factors].filter((factor) => factor.contribution > 0).sort((a, b) => b.contribution - a.contribution || a.factorId.localeCompare(b.factorId)).slice(0, 3).map((factor) => factor.factorId);
|
|
4552
4784
|
return {
|
|
4553
4785
|
targetType,
|
|
4554
4786
|
targetId,
|
|
4555
|
-
totalScore:
|
|
4556
|
-
normalizedScore:
|
|
4787
|
+
totalScore: round46(totalScore),
|
|
4788
|
+
normalizedScore: round46(normalizedScore),
|
|
4557
4789
|
factors,
|
|
4558
4790
|
dominantFactors,
|
|
4559
4791
|
reductionLevers: buildReductionLevers(factors)
|
|
@@ -4627,14 +4859,14 @@ var computeDependencyScores = (external, config) => {
|
|
|
4627
4859
|
].filter((value) => value !== null).length;
|
|
4628
4860
|
const confidence = toUnitInterval((0.5 + availableMetricCount * 0.125) * metadataConfidence);
|
|
4629
4861
|
dependencyContexts.set(dependency.name, {
|
|
4630
|
-
signalScore:
|
|
4631
|
-
stalenessRisk:
|
|
4632
|
-
maintainerConcentrationRisk:
|
|
4633
|
-
transitiveBurdenRisk:
|
|
4634
|
-
centralityRisk:
|
|
4635
|
-
chainDepthRisk:
|
|
4636
|
-
busFactorRisk:
|
|
4637
|
-
popularityDampener:
|
|
4862
|
+
signalScore: round46(signalScore),
|
|
4863
|
+
stalenessRisk: round46(stalenessRisk),
|
|
4864
|
+
maintainerConcentrationRisk: round46(maintainerConcentrationRisk),
|
|
4865
|
+
transitiveBurdenRisk: round46(transitiveBurdenRisk),
|
|
4866
|
+
centralityRisk: round46(centralityRisk),
|
|
4867
|
+
chainDepthRisk: round46(chainDepthRisk),
|
|
4868
|
+
busFactorRisk: round46(busFactorRisk),
|
|
4869
|
+
popularityDampener: round46(popularityDampener),
|
|
4638
4870
|
rawMetrics: {
|
|
4639
4871
|
daysSinceLastRelease: dependency.daysSinceLastRelease,
|
|
4640
4872
|
maintainerCount: dependency.maintainerCount,
|
|
@@ -4644,12 +4876,12 @@ var computeDependencyScores = (external, config) => {
|
|
|
4644
4876
|
busFactor: dependency.busFactor,
|
|
4645
4877
|
weeklyDownloads: dependency.weeklyDownloads
|
|
4646
4878
|
},
|
|
4647
|
-
confidence:
|
|
4879
|
+
confidence: round46(confidence)
|
|
4648
4880
|
});
|
|
4649
4881
|
return {
|
|
4650
4882
|
dependency: dependency.name,
|
|
4651
|
-
score:
|
|
4652
|
-
normalizedScore:
|
|
4883
|
+
score: round46(normalizedScore * 100),
|
|
4884
|
+
normalizedScore: round46(normalizedScore),
|
|
4653
4885
|
ownRiskSignals: dependency.ownRiskSignals,
|
|
4654
4886
|
inheritedRiskSignals: dependency.inheritedRiskSignals
|
|
4655
4887
|
};
|
|
@@ -4658,7 +4890,7 @@ var computeDependencyScores = (external, config) => {
|
|
|
4658
4890
|
);
|
|
4659
4891
|
const normalizedValues = dependencyScores.map((score) => score.normalizedScore);
|
|
4660
4892
|
const highDependencyRisk = dependencyScores.length === 0 ? 0 : percentile(normalizedValues, config.externalDimension.topDependencyPercentile);
|
|
4661
|
-
const averageDependencyRisk =
|
|
4893
|
+
const averageDependencyRisk = average2(normalizedValues);
|
|
4662
4894
|
const depthRisk = halfLifeRisk(
|
|
4663
4895
|
external.metrics.dependencyDepth,
|
|
4664
4896
|
config.externalDimension.dependencyDepthHalfLife
|
|
@@ -4668,7 +4900,7 @@ var computeDependencyScores = (external, config) => {
|
|
|
4668
4900
|
);
|
|
4669
4901
|
return {
|
|
4670
4902
|
dependencyScores,
|
|
4671
|
-
repositoryExternalPressure:
|
|
4903
|
+
repositoryExternalPressure: round46(repositoryExternalPressure),
|
|
4672
4904
|
dependencyContexts
|
|
4673
4905
|
};
|
|
4674
4906
|
};
|
|
@@ -4729,11 +4961,11 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
|
|
|
4729
4961
|
continue;
|
|
4730
4962
|
}
|
|
4731
4963
|
files.sort((a, b) => a.localeCompare(b));
|
|
4732
|
-
const averageRisk =
|
|
4964
|
+
const averageRisk = average2(
|
|
4733
4965
|
files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
|
|
4734
4966
|
);
|
|
4735
4967
|
const cycleSizeRisk = toUnitInterval((files.length - 1) / 5);
|
|
4736
|
-
const score =
|
|
4968
|
+
const score = round46(toUnitInterval(averageRisk * 0.75 + cycleSizeRisk * 0.25) * 100);
|
|
4737
4969
|
cycleClusterCount += 1;
|
|
4738
4970
|
clusters.push({
|
|
4739
4971
|
id: `cycle:${cycleClusterCount}`,
|
|
@@ -4803,11 +5035,11 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
|
|
|
4803
5035
|
const componentPairs = selectedPairs.filter(
|
|
4804
5036
|
(pair) => fileSet.has(pair.fileA) && fileSet.has(pair.fileB)
|
|
4805
5037
|
);
|
|
4806
|
-
const meanFileRisk =
|
|
5038
|
+
const meanFileRisk = average2(
|
|
4807
5039
|
files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
|
|
4808
5040
|
);
|
|
4809
|
-
const meanCoupling =
|
|
4810
|
-
const score =
|
|
5041
|
+
const meanCoupling = average2(componentPairs.map((pair) => pair.couplingScore));
|
|
5042
|
+
const score = round46(toUnitInterval(meanFileRisk * 0.65 + meanCoupling * 0.35) * 100);
|
|
4811
5043
|
couplingClusterCount += 1;
|
|
4812
5044
|
clusters.push({
|
|
4813
5045
|
id: `coupling:${couplingClusterCount}`,
|
|
@@ -4918,21 +5150,21 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
4918
5150
|
const normalizedScore = saturatingComposite(baseline, interactions);
|
|
4919
5151
|
return {
|
|
4920
5152
|
file: filePath,
|
|
4921
|
-
score:
|
|
4922
|
-
normalizedScore:
|
|
5153
|
+
score: round46(normalizedScore * 100),
|
|
5154
|
+
normalizedScore: round46(normalizedScore),
|
|
4923
5155
|
factors: {
|
|
4924
|
-
structural:
|
|
4925
|
-
evolution:
|
|
4926
|
-
external:
|
|
5156
|
+
structural: round46(structuralFactor),
|
|
5157
|
+
evolution: round46(evolutionFactor),
|
|
5158
|
+
external: round46(externalFactor)
|
|
4927
5159
|
},
|
|
4928
|
-
structuralCentrality:
|
|
5160
|
+
structuralCentrality: round46(structuralCentrality),
|
|
4929
5161
|
traceTerms: {
|
|
4930
|
-
structuralBase:
|
|
4931
|
-
evolutionBase:
|
|
4932
|
-
externalBase:
|
|
4933
|
-
interactionStructuralEvolution:
|
|
4934
|
-
interactionCentralInstability:
|
|
4935
|
-
interactionDependencyAmplification:
|
|
5162
|
+
structuralBase: round46(structuralBase),
|
|
5163
|
+
evolutionBase: round46(evolutionBase),
|
|
5164
|
+
externalBase: round46(externalBase),
|
|
5165
|
+
interactionStructuralEvolution: round46(interactionStructuralEvolution),
|
|
5166
|
+
interactionCentralInstability: round46(interactionCentralInstability),
|
|
5167
|
+
interactionDependencyAmplification: round46(interactionDependencyAmplification)
|
|
4936
5168
|
},
|
|
4937
5169
|
rawMetrics: {
|
|
4938
5170
|
fanIn: file.fanIn,
|
|
@@ -4944,19 +5176,19 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
4944
5176
|
recentVolatility: evolutionMetrics?.recentVolatility ?? null,
|
|
4945
5177
|
topAuthorShare: evolutionMetrics?.topAuthorShare ?? null,
|
|
4946
5178
|
busFactor: evolutionMetrics?.busFactor ?? null,
|
|
4947
|
-
dependencyAffinity:
|
|
4948
|
-
repositoryExternalPressure:
|
|
4949
|
-
structuralAttenuation:
|
|
5179
|
+
dependencyAffinity: round46(dependencyAffinity),
|
|
5180
|
+
repositoryExternalPressure: round46(dependencyComputation.repositoryExternalPressure),
|
|
5181
|
+
structuralAttenuation: round46(structuralAttenuation)
|
|
4950
5182
|
},
|
|
4951
5183
|
normalizedMetrics: {
|
|
4952
|
-
fanInRisk:
|
|
4953
|
-
fanOutRisk:
|
|
4954
|
-
depthRisk:
|
|
4955
|
-
frequencyRisk:
|
|
4956
|
-
churnRisk:
|
|
4957
|
-
volatilityRisk:
|
|
4958
|
-
ownershipConcentrationRisk:
|
|
4959
|
-
busFactorRisk:
|
|
5184
|
+
fanInRisk: round46(fanInRisk),
|
|
5185
|
+
fanOutRisk: round46(fanOutRisk),
|
|
5186
|
+
depthRisk: round46(depthRisk),
|
|
5187
|
+
frequencyRisk: round46(frequencyRisk),
|
|
5188
|
+
churnRisk: round46(churnRisk),
|
|
5189
|
+
volatilityRisk: round46(volatilityRisk),
|
|
5190
|
+
ownershipConcentrationRisk: round46(ownershipConcentrationRisk),
|
|
5191
|
+
busFactorRisk: round46(busFactorRisk)
|
|
4960
5192
|
}
|
|
4961
5193
|
};
|
|
4962
5194
|
}).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
@@ -5082,29 +5314,29 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
5082
5314
|
moduleFiles.set(moduleName, values);
|
|
5083
5315
|
}
|
|
5084
5316
|
const moduleScores = [...moduleFiles.entries()].map(([module, values]) => {
|
|
5085
|
-
const averageScore =
|
|
5317
|
+
const averageScore = average2(values);
|
|
5086
5318
|
const peakScore = values.reduce((max, value) => Math.max(max, value), 0);
|
|
5087
5319
|
const normalizedScore = toUnitInterval(averageScore * 0.65 + peakScore * 0.35);
|
|
5088
5320
|
return {
|
|
5089
5321
|
module,
|
|
5090
|
-
score:
|
|
5091
|
-
normalizedScore:
|
|
5322
|
+
score: round46(normalizedScore * 100),
|
|
5323
|
+
normalizedScore: round46(normalizedScore),
|
|
5092
5324
|
fileCount: values.length
|
|
5093
5325
|
};
|
|
5094
5326
|
}).sort((a, b) => b.score - a.score || a.module.localeCompare(b.module));
|
|
5095
5327
|
if (collector !== void 0) {
|
|
5096
5328
|
for (const [module, values] of moduleFiles.entries()) {
|
|
5097
|
-
const averageScore =
|
|
5329
|
+
const averageScore = average2(values);
|
|
5098
5330
|
const peakScore = values.reduce((max, value) => Math.max(max, value), 0);
|
|
5099
5331
|
const normalizedScore = toUnitInterval(averageScore * 0.65 + peakScore * 0.35);
|
|
5100
|
-
const totalScore =
|
|
5332
|
+
const totalScore = round46(normalizedScore * 100);
|
|
5101
5333
|
const factors = buildFactorTraces(totalScore, [
|
|
5102
5334
|
{
|
|
5103
5335
|
factorId: "module.average_file_risk",
|
|
5104
5336
|
family: "composite",
|
|
5105
5337
|
strength: averageScore * 0.65,
|
|
5106
|
-
rawMetrics: { averageFileRisk:
|
|
5107
|
-
normalizedMetrics: { normalizedModuleRisk:
|
|
5338
|
+
rawMetrics: { averageFileRisk: round46(averageScore), fileCount: values.length },
|
|
5339
|
+
normalizedMetrics: { normalizedModuleRisk: round46(normalizedScore) },
|
|
5108
5340
|
weight: 0.65,
|
|
5109
5341
|
amplification: null,
|
|
5110
5342
|
evidence: [{ kind: "repository_metric", metric: "moduleAggregation.average" }],
|
|
@@ -5114,8 +5346,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
5114
5346
|
factorId: "module.peak_file_risk",
|
|
5115
5347
|
family: "composite",
|
|
5116
5348
|
strength: peakScore * 0.35,
|
|
5117
|
-
rawMetrics: { peakFileRisk:
|
|
5118
|
-
normalizedMetrics: { normalizedModuleRisk:
|
|
5349
|
+
rawMetrics: { peakFileRisk: round46(peakScore), fileCount: values.length },
|
|
5350
|
+
normalizedMetrics: { normalizedModuleRisk: round46(normalizedScore) },
|
|
5119
5351
|
weight: 0.35,
|
|
5120
5352
|
amplification: null,
|
|
5121
5353
|
evidence: [{ kind: "repository_metric", metric: "moduleAggregation.peak" }],
|
|
@@ -5138,12 +5370,12 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
5138
5370
|
const normalizedZoneScore = toUnitInterval(intensity * 0.7 + fileScore.normalizedScore * 0.3);
|
|
5139
5371
|
return {
|
|
5140
5372
|
file: fileScore.file,
|
|
5141
|
-
score:
|
|
5373
|
+
score: round46(normalizedZoneScore * 100),
|
|
5142
5374
|
externalPressure: fileScore.factors.external
|
|
5143
5375
|
};
|
|
5144
5376
|
}).filter((zone) => external.available && zone.externalPressure >= pressureThreshold).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file)).slice(0, config.amplificationZone.maxZones).map((zone) => ({
|
|
5145
5377
|
...zone,
|
|
5146
|
-
externalPressure:
|
|
5378
|
+
externalPressure: round46(zone.externalPressure)
|
|
5147
5379
|
}));
|
|
5148
5380
|
if (collector !== void 0 && external.available) {
|
|
5149
5381
|
const dependencyByName = new Map(
|
|
@@ -5256,16 +5488,16 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
5256
5488
|
);
|
|
5257
5489
|
}
|
|
5258
5490
|
}
|
|
5259
|
-
const structuralDimension =
|
|
5260
|
-
const evolutionDimension =
|
|
5491
|
+
const structuralDimension = average2(fileScores.map((fileScore) => fileScore.factors.structural));
|
|
5492
|
+
const evolutionDimension = average2(fileScores.map((fileScore) => fileScore.factors.evolution));
|
|
5261
5493
|
const externalDimension = dependencyComputation.repositoryExternalPressure;
|
|
5262
5494
|
const topCentralSlice = Math.max(1, Math.ceil(fileRiskContexts.length * 0.1));
|
|
5263
|
-
const criticalInstability =
|
|
5495
|
+
const criticalInstability = average2(
|
|
5264
5496
|
[...fileRiskContexts].sort(
|
|
5265
5497
|
(a, b) => b.structuralCentrality * b.factors.evolution - a.structuralCentrality * a.factors.evolution || a.file.localeCompare(b.file)
|
|
5266
5498
|
).slice(0, topCentralSlice).map((context) => context.structuralCentrality * context.factors.evolution)
|
|
5267
5499
|
);
|
|
5268
|
-
const dependencyAmplification =
|
|
5500
|
+
const dependencyAmplification = average2(
|
|
5269
5501
|
dependencyAmplificationZones.map(
|
|
5270
5502
|
(zone) => toUnitInterval(zone.externalPressure * zone.score / 100)
|
|
5271
5503
|
)
|
|
@@ -5276,15 +5508,15 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
5276
5508
|
criticalInstability * config.interactionWeights.centralInstability,
|
|
5277
5509
|
dependencyAmplification * config.interactionWeights.dependencyAmplification
|
|
5278
5510
|
]);
|
|
5279
|
-
const riskScore =
|
|
5511
|
+
const riskScore = round46(repositoryNormalizedScore * 100);
|
|
5280
5512
|
if (collector !== void 0) {
|
|
5281
5513
|
const repositoryFactors = buildFactorTraces(riskScore, [
|
|
5282
5514
|
{
|
|
5283
5515
|
factorId: "repository.structural",
|
|
5284
5516
|
family: "structural",
|
|
5285
5517
|
strength: structuralDimension * dimensionWeights.structural,
|
|
5286
|
-
rawMetrics: { structuralDimension:
|
|
5287
|
-
normalizedMetrics: { dimensionWeight:
|
|
5518
|
+
rawMetrics: { structuralDimension: round46(structuralDimension) },
|
|
5519
|
+
normalizedMetrics: { dimensionWeight: round46(dimensionWeights.structural) },
|
|
5288
5520
|
weight: dimensionWeights.structural,
|
|
5289
5521
|
amplification: null,
|
|
5290
5522
|
evidence: [{ kind: "repository_metric", metric: "structuralDimension" }],
|
|
@@ -5294,8 +5526,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
5294
5526
|
factorId: "repository.evolution",
|
|
5295
5527
|
family: "evolution",
|
|
5296
5528
|
strength: evolutionDimension * dimensionWeights.evolution,
|
|
5297
|
-
rawMetrics: { evolutionDimension:
|
|
5298
|
-
normalizedMetrics: { dimensionWeight:
|
|
5529
|
+
rawMetrics: { evolutionDimension: round46(evolutionDimension) },
|
|
5530
|
+
normalizedMetrics: { dimensionWeight: round46(dimensionWeights.evolution) },
|
|
5299
5531
|
weight: dimensionWeights.evolution,
|
|
5300
5532
|
amplification: null,
|
|
5301
5533
|
evidence: [{ kind: "repository_metric", metric: "evolutionDimension" }],
|
|
@@ -5305,8 +5537,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
5305
5537
|
factorId: "repository.external",
|
|
5306
5538
|
family: "external",
|
|
5307
5539
|
strength: externalDimension * dimensionWeights.external,
|
|
5308
|
-
rawMetrics: { externalDimension:
|
|
5309
|
-
normalizedMetrics: { dimensionWeight:
|
|
5540
|
+
rawMetrics: { externalDimension: round46(externalDimension) },
|
|
5541
|
+
normalizedMetrics: { dimensionWeight: round46(dimensionWeights.external) },
|
|
5310
5542
|
weight: dimensionWeights.external,
|
|
5311
5543
|
amplification: null,
|
|
5312
5544
|
evidence: [{ kind: "repository_metric", metric: "externalDimension" }],
|
|
@@ -5317,19 +5549,19 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
5317
5549
|
family: "composite",
|
|
5318
5550
|
strength: structuralDimension * evolutionDimension * config.interactionWeights.structuralEvolution + criticalInstability * config.interactionWeights.centralInstability + dependencyAmplification * config.interactionWeights.dependencyAmplification,
|
|
5319
5551
|
rawMetrics: {
|
|
5320
|
-
structuralEvolution:
|
|
5552
|
+
structuralEvolution: round46(
|
|
5321
5553
|
structuralDimension * evolutionDimension * config.interactionWeights.structuralEvolution
|
|
5322
5554
|
),
|
|
5323
|
-
centralInstability:
|
|
5555
|
+
centralInstability: round46(
|
|
5324
5556
|
criticalInstability * config.interactionWeights.centralInstability
|
|
5325
5557
|
),
|
|
5326
|
-
dependencyAmplification:
|
|
5558
|
+
dependencyAmplification: round46(
|
|
5327
5559
|
dependencyAmplification * config.interactionWeights.dependencyAmplification
|
|
5328
5560
|
)
|
|
5329
5561
|
},
|
|
5330
5562
|
normalizedMetrics: {
|
|
5331
|
-
criticalInstability:
|
|
5332
|
-
dependencyAmplification:
|
|
5563
|
+
criticalInstability: round46(criticalInstability),
|
|
5564
|
+
dependencyAmplification: round46(dependencyAmplification)
|
|
5333
5565
|
},
|
|
5334
5566
|
weight: null,
|
|
5335
5567
|
amplification: config.interactionWeights.structuralEvolution + config.interactionWeights.centralInstability + config.interactionWeights.dependencyAmplification,
|
|
@@ -5349,7 +5581,7 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
5349
5581
|
}
|
|
5350
5582
|
return {
|
|
5351
5583
|
riskScore,
|
|
5352
|
-
normalizedScore:
|
|
5584
|
+
normalizedScore: round46(repositoryNormalizedScore),
|
|
5353
5585
|
hotspots,
|
|
5354
5586
|
fragileClusters,
|
|
5355
5587
|
dependencyAmplificationZones,
|
|
@@ -5472,6 +5704,28 @@ var evaluateRepositoryRisk = (input, options = {}) => {
|
|
|
5472
5704
|
};
|
|
5473
5705
|
};
|
|
5474
5706
|
|
|
5707
|
+
// src/application/todo-fixme-counter.ts
|
|
5708
|
+
import * as ts2 from "typescript";
|
|
5709
|
+
var markerRegex = /\b(?:TODO|FIXME)\b/gi;
|
|
5710
|
+
var countMarkers = (text) => text.match(markerRegex)?.length ?? 0;
|
|
5711
|
+
var countTodoFixmeInComments = (content) => {
|
|
5712
|
+
const scanner = ts2.createScanner(
|
|
5713
|
+
ts2.ScriptTarget.Latest,
|
|
5714
|
+
false,
|
|
5715
|
+
ts2.LanguageVariant.Standard,
|
|
5716
|
+
content
|
|
5717
|
+
);
|
|
5718
|
+
let total = 0;
|
|
5719
|
+
let token = scanner.scan();
|
|
5720
|
+
while (token !== ts2.SyntaxKind.EndOfFileToken) {
|
|
5721
|
+
if (token === ts2.SyntaxKind.SingleLineCommentTrivia || token === ts2.SyntaxKind.MultiLineCommentTrivia) {
|
|
5722
|
+
total += countMarkers(scanner.getTokenText());
|
|
5723
|
+
}
|
|
5724
|
+
token = scanner.scan();
|
|
5725
|
+
}
|
|
5726
|
+
return total;
|
|
5727
|
+
};
|
|
5728
|
+
|
|
5475
5729
|
// src/application/run-analyze-command.ts
|
|
5476
5730
|
var resolveTargetPath = (inputPath, cwd) => resolve3(cwd, inputPath ?? ".");
|
|
5477
5731
|
var riskProfileConfig = {
|
|
@@ -5600,6 +5854,18 @@ var createEvolutionProgressReporter = (logger) => {
|
|
|
5600
5854
|
}
|
|
5601
5855
|
};
|
|
5602
5856
|
};
|
|
5857
|
+
var collectTodoFixmeCount = async (targetPath, structural) => {
|
|
5858
|
+
const filePaths2 = [...structural.files].map((file) => file.relativePath).sort((a, b) => a.localeCompare(b));
|
|
5859
|
+
let total = 0;
|
|
5860
|
+
for (const relativePath of filePaths2) {
|
|
5861
|
+
try {
|
|
5862
|
+
const content = await readFile2(join4(targetPath, relativePath), "utf8");
|
|
5863
|
+
total += countTodoFixmeInComments(content);
|
|
5864
|
+
} catch {
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
return total;
|
|
5868
|
+
};
|
|
5603
5869
|
var collectAnalysisInputs = async (inputPath, authorIdentityMode, options = {}, logger = createSilentLogger()) => {
|
|
5604
5870
|
const invocationCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
5605
5871
|
const targetPath = resolveTargetPath(inputPath, invocationCwd);
|
|
@@ -5642,10 +5908,14 @@ var collectAnalysisInputs = async (inputPath, authorIdentityMode, options = {},
|
|
|
5642
5908
|
} else {
|
|
5643
5909
|
logger.warn(`external analysis unavailable: ${external.reason}`);
|
|
5644
5910
|
}
|
|
5911
|
+
logger.info("collecting quality text signals");
|
|
5912
|
+
const todoFixmeCount = await collectTodoFixmeCount(targetPath, structural);
|
|
5913
|
+
logger.debug(`quality text signals: todoFixmeCount=${todoFixmeCount}`);
|
|
5645
5914
|
return {
|
|
5646
5915
|
structural,
|
|
5647
5916
|
evolution,
|
|
5648
|
-
external
|
|
5917
|
+
external,
|
|
5918
|
+
todoFixmeCount
|
|
5649
5919
|
};
|
|
5650
5920
|
};
|
|
5651
5921
|
var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logger = createSilentLogger()) => {
|
|
@@ -5658,18 +5928,30 @@ var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logg
|
|
|
5658
5928
|
logger.info("computing risk summary");
|
|
5659
5929
|
const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
|
|
5660
5930
|
const risk = computeRepositoryRiskSummary({
|
|
5661
|
-
|
|
5931
|
+
structural: analysisInputs.structural,
|
|
5932
|
+
evolution: analysisInputs.evolution,
|
|
5933
|
+
external: analysisInputs.external,
|
|
5662
5934
|
...riskConfig === void 0 ? {} : { config: riskConfig }
|
|
5663
5935
|
});
|
|
5664
|
-
|
|
5936
|
+
const quality = computeRepositoryQualitySummary({
|
|
5937
|
+
structural: analysisInputs.structural,
|
|
5938
|
+
evolution: analysisInputs.evolution,
|
|
5939
|
+
todoFixmeCount: analysisInputs.todoFixmeCount
|
|
5940
|
+
});
|
|
5941
|
+
logger.info(
|
|
5942
|
+
`analysis completed (riskScore=${risk.riskScore}, qualityScore=${quality.qualityScore})`
|
|
5943
|
+
);
|
|
5665
5944
|
return {
|
|
5666
|
-
|
|
5667
|
-
|
|
5945
|
+
structural: analysisInputs.structural,
|
|
5946
|
+
evolution: analysisInputs.evolution,
|
|
5947
|
+
external: analysisInputs.external,
|
|
5948
|
+
risk,
|
|
5949
|
+
quality
|
|
5668
5950
|
};
|
|
5669
5951
|
};
|
|
5670
5952
|
|
|
5671
5953
|
// src/application/run-check-command.ts
|
|
5672
|
-
import { readFile as
|
|
5954
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
5673
5955
|
|
|
5674
5956
|
// src/application/build-analysis-snapshot.ts
|
|
5675
5957
|
var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logger) => {
|
|
@@ -5684,14 +5966,23 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
|
|
|
5684
5966
|
const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
|
|
5685
5967
|
const evaluation = evaluateRepositoryRisk(
|
|
5686
5968
|
{
|
|
5687
|
-
|
|
5969
|
+
structural: analysisInputs.structural,
|
|
5970
|
+
evolution: analysisInputs.evolution,
|
|
5971
|
+
external: analysisInputs.external,
|
|
5688
5972
|
...riskConfig === void 0 ? {} : { config: riskConfig }
|
|
5689
5973
|
},
|
|
5690
5974
|
{ explain: options.includeTrace }
|
|
5691
5975
|
);
|
|
5692
5976
|
const summary = {
|
|
5693
|
-
|
|
5694
|
-
|
|
5977
|
+
structural: analysisInputs.structural,
|
|
5978
|
+
evolution: analysisInputs.evolution,
|
|
5979
|
+
external: analysisInputs.external,
|
|
5980
|
+
risk: evaluation.summary,
|
|
5981
|
+
quality: computeRepositoryQualitySummary({
|
|
5982
|
+
structural: analysisInputs.structural,
|
|
5983
|
+
evolution: analysisInputs.evolution,
|
|
5984
|
+
todoFixmeCount: analysisInputs.todoFixmeCount
|
|
5985
|
+
})
|
|
5695
5986
|
};
|
|
5696
5987
|
return createSnapshot({
|
|
5697
5988
|
analysis: summary,
|
|
@@ -5743,7 +6034,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
|
|
|
5743
6034
|
let diff;
|
|
5744
6035
|
if (options.baselinePath !== void 0) {
|
|
5745
6036
|
logger.info(`loading baseline snapshot: ${options.baselinePath}`);
|
|
5746
|
-
const baselineRaw = await
|
|
6037
|
+
const baselineRaw = await readFile3(options.baselinePath, "utf8");
|
|
5747
6038
|
try {
|
|
5748
6039
|
baseline = parseSnapshot(baselineRaw);
|
|
5749
6040
|
} catch (error) {
|
|
@@ -5782,7 +6073,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
|
|
|
5782
6073
|
};
|
|
5783
6074
|
|
|
5784
6075
|
// src/application/run-ci-command.ts
|
|
5785
|
-
import { readFile as
|
|
6076
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
5786
6077
|
import { relative as relative2, resolve as resolve4 } from "path";
|
|
5787
6078
|
var isPathOutsideBase = (value) => {
|
|
5788
6079
|
return value === ".." || value.startsWith("../") || value.startsWith("..\\");
|
|
@@ -5882,7 +6173,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
|
|
|
5882
6173
|
diff = compareSnapshots(current, baseline);
|
|
5883
6174
|
} else if (options.baselinePath !== void 0) {
|
|
5884
6175
|
logger.info(`loading baseline snapshot: ${options.baselinePath}`);
|
|
5885
|
-
const baselineRaw = await
|
|
6176
|
+
const baselineRaw = await readFile4(options.baselinePath, "utf8");
|
|
5886
6177
|
try {
|
|
5887
6178
|
baseline = parseSnapshot(baselineRaw);
|
|
5888
6179
|
} catch (error) {
|
|
@@ -5930,7 +6221,7 @@ ${ciMarkdown}`;
|
|
|
5930
6221
|
};
|
|
5931
6222
|
|
|
5932
6223
|
// src/application/run-report-command.ts
|
|
5933
|
-
import { readFile as
|
|
6224
|
+
import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
5934
6225
|
var runReportCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
|
|
5935
6226
|
logger.info("building analysis snapshot");
|
|
5936
6227
|
const current = await buildAnalysisSnapshot(
|
|
@@ -5952,7 +6243,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
|
|
|
5952
6243
|
report = createReport(current);
|
|
5953
6244
|
} else {
|
|
5954
6245
|
logger.info(`loading baseline snapshot: ${options.comparePath}`);
|
|
5955
|
-
const baselineRaw = await
|
|
6246
|
+
const baselineRaw = await readFile5(options.comparePath, "utf8");
|
|
5956
6247
|
const baseline = parseSnapshot(baselineRaw);
|
|
5957
6248
|
const diff = compareSnapshots(current, baseline);
|
|
5958
6249
|
report = createReport(current, diff);
|
|
@@ -5998,7 +6289,9 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
|
|
|
5998
6289
|
const riskConfig = resolveRiskConfigForProfile(options.riskProfile);
|
|
5999
6290
|
const evaluation = evaluateRepositoryRisk(
|
|
6000
6291
|
{
|
|
6001
|
-
|
|
6292
|
+
structural: analysisInputs.structural,
|
|
6293
|
+
evolution: analysisInputs.evolution,
|
|
6294
|
+
external: analysisInputs.external,
|
|
6002
6295
|
...riskConfig === void 0 ? {} : { config: riskConfig }
|
|
6003
6296
|
},
|
|
6004
6297
|
{ explain: true }
|
|
@@ -6007,10 +6300,19 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
|
|
|
6007
6300
|
throw new Error("risk trace unavailable");
|
|
6008
6301
|
}
|
|
6009
6302
|
const summary = {
|
|
6010
|
-
|
|
6011
|
-
|
|
6303
|
+
structural: analysisInputs.structural,
|
|
6304
|
+
evolution: analysisInputs.evolution,
|
|
6305
|
+
external: analysisInputs.external,
|
|
6306
|
+
risk: evaluation.summary,
|
|
6307
|
+
quality: computeRepositoryQualitySummary({
|
|
6308
|
+
structural: analysisInputs.structural,
|
|
6309
|
+
evolution: analysisInputs.evolution,
|
|
6310
|
+
todoFixmeCount: analysisInputs.todoFixmeCount
|
|
6311
|
+
})
|
|
6012
6312
|
};
|
|
6013
|
-
logger.info(
|
|
6313
|
+
logger.info(
|
|
6314
|
+
`explanation completed (riskScore=${summary.risk.riskScore}, qualityScore=${summary.quality.qualityScore})`
|
|
6315
|
+
);
|
|
6014
6316
|
return {
|
|
6015
6317
|
summary,
|
|
6016
6318
|
trace: evaluation.trace,
|
|
@@ -6029,6 +6331,122 @@ var parseRecentWindowDays = (value) => {
|
|
|
6029
6331
|
}
|
|
6030
6332
|
return parsed;
|
|
6031
6333
|
};
|
|
6334
|
+
var stripLeadingMarkdownHeading = (value, heading) => {
|
|
6335
|
+
const prefix = `${heading}
|
|
6336
|
+
`;
|
|
6337
|
+
if (value.startsWith(prefix)) {
|
|
6338
|
+
return value.slice(prefix.length).trimStart();
|
|
6339
|
+
}
|
|
6340
|
+
return value;
|
|
6341
|
+
};
|
|
6342
|
+
var extractExplainTextSummary = (text) => {
|
|
6343
|
+
const splitIndex = text.search(/\n\n(?:file|module|dependency|repository): /);
|
|
6344
|
+
if (splitIndex < 0) {
|
|
6345
|
+
return text.trim();
|
|
6346
|
+
}
|
|
6347
|
+
return text.slice(0, splitIndex).trim();
|
|
6348
|
+
};
|
|
6349
|
+
var extractExplainMarkdownSummary = (markdown) => {
|
|
6350
|
+
const splitIndex = markdown.search(/\n\n## (?:file|module|dependency|repository): /);
|
|
6351
|
+
if (splitIndex < 0) {
|
|
6352
|
+
return markdown.trim();
|
|
6353
|
+
}
|
|
6354
|
+
return markdown.slice(0, splitIndex).trim();
|
|
6355
|
+
};
|
|
6356
|
+
var extractSummaryValue = (summary, key) => {
|
|
6357
|
+
const prefix = `${key}: `;
|
|
6358
|
+
for (const rawLine of summary.split("\n")) {
|
|
6359
|
+
const line = rawLine.trimStart();
|
|
6360
|
+
if (line.startsWith(prefix)) {
|
|
6361
|
+
return line.slice(prefix.length).trim();
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
return void 0;
|
|
6365
|
+
};
|
|
6366
|
+
var renderReportHighlightsText = (report) => {
|
|
6367
|
+
const lines = [];
|
|
6368
|
+
lines.push("Repository Summary");
|
|
6369
|
+
lines.push(` target: ${report.repository.targetPath}`);
|
|
6370
|
+
lines.push(` riskScore: ${report.repository.riskScore}`);
|
|
6371
|
+
lines.push(` qualityScore: ${report.quality.qualityScore}`);
|
|
6372
|
+
lines.push(` normalizedScore: ${report.repository.normalizedScore}`);
|
|
6373
|
+
lines.push(` riskTier: ${report.repository.riskTier}`);
|
|
6374
|
+
lines.push("");
|
|
6375
|
+
lines.push("Top Hotspots");
|
|
6376
|
+
for (const hotspot of report.hotspots.slice(0, 5)) {
|
|
6377
|
+
lines.push(` - ${hotspot.target} | score=${hotspot.score}`);
|
|
6378
|
+
lines.push(` priority actions: ${hotspot.suggestedActions.join(" | ") || "none"}`);
|
|
6379
|
+
}
|
|
6380
|
+
return lines.join("\n");
|
|
6381
|
+
};
|
|
6382
|
+
var renderReportHighlightsMarkdown = (report) => {
|
|
6383
|
+
const lines = [];
|
|
6384
|
+
lines.push("## Repository Summary");
|
|
6385
|
+
lines.push(`- target: \`${report.repository.targetPath}\``);
|
|
6386
|
+
lines.push(`- riskScore: \`${report.repository.riskScore}\``);
|
|
6387
|
+
lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
|
|
6388
|
+
lines.push(`- normalizedScore: \`${report.repository.normalizedScore}\``);
|
|
6389
|
+
lines.push(`- riskTier: \`${report.repository.riskTier}\``);
|
|
6390
|
+
lines.push("");
|
|
6391
|
+
lines.push("## Top Hotspots");
|
|
6392
|
+
for (const hotspot of report.hotspots.slice(0, 5)) {
|
|
6393
|
+
lines.push(`- **${hotspot.target}** (score: \`${hotspot.score}\`)`);
|
|
6394
|
+
lines.push(` - priority actions: ${hotspot.suggestedActions.join(" | ") || "none"}`);
|
|
6395
|
+
}
|
|
6396
|
+
return lines.join("\n");
|
|
6397
|
+
};
|
|
6398
|
+
var renderCompactText = (report, explainSummary) => {
|
|
6399
|
+
const lines = [];
|
|
6400
|
+
lines.push("CodeSentinel Run (compact)");
|
|
6401
|
+
lines.push("");
|
|
6402
|
+
lines.push("Repository");
|
|
6403
|
+
lines.push(` target: ${report.repository.targetPath}`);
|
|
6404
|
+
lines.push(` riskScore: ${report.repository.riskScore}`);
|
|
6405
|
+
lines.push(` qualityScore: ${report.quality.qualityScore}`);
|
|
6406
|
+
lines.push(` riskTier: ${report.repository.riskTier}`);
|
|
6407
|
+
lines.push(
|
|
6408
|
+
` 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"}`
|
|
6409
|
+
);
|
|
6410
|
+
lines.push("");
|
|
6411
|
+
lines.push(
|
|
6412
|
+
`Key Drivers: ${extractSummaryValue(explainSummary, "key drivers") ?? "insufficient data"}`
|
|
6413
|
+
);
|
|
6414
|
+
lines.push(
|
|
6415
|
+
`Priority Actions: ${extractSummaryValue(explainSummary, "priority actions") ?? "insufficient data"}`
|
|
6416
|
+
);
|
|
6417
|
+
lines.push("");
|
|
6418
|
+
lines.push("Top Hotspots");
|
|
6419
|
+
for (const hotspot of report.hotspots.slice(0, 3)) {
|
|
6420
|
+
lines.push(` - ${hotspot.target} | score=${hotspot.score}`);
|
|
6421
|
+
}
|
|
6422
|
+
return lines.join("\n");
|
|
6423
|
+
};
|
|
6424
|
+
var renderCompactMarkdown = (report, explainSummary) => {
|
|
6425
|
+
const lines = [];
|
|
6426
|
+
lines.push("# CodeSentinel Run (compact)");
|
|
6427
|
+
lines.push("");
|
|
6428
|
+
lines.push("## Repository");
|
|
6429
|
+
lines.push(`- target: \`${report.repository.targetPath}\``);
|
|
6430
|
+
lines.push(`- riskScore: \`${report.repository.riskScore}\``);
|
|
6431
|
+
lines.push(`- qualityScore: \`${report.quality.qualityScore}\``);
|
|
6432
|
+
lines.push(`- riskTier: \`${report.repository.riskTier}\``);
|
|
6433
|
+
lines.push(
|
|
6434
|
+
`- 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"}\``
|
|
6435
|
+
);
|
|
6436
|
+
lines.push("");
|
|
6437
|
+
lines.push(
|
|
6438
|
+
`- key drivers: ${extractSummaryValue(explainSummary, "- key drivers") ?? "insufficient data"}`
|
|
6439
|
+
);
|
|
6440
|
+
lines.push(
|
|
6441
|
+
`- priority actions: ${extractSummaryValue(explainSummary, "- priority actions") ?? "insufficient data"}`
|
|
6442
|
+
);
|
|
6443
|
+
lines.push("");
|
|
6444
|
+
lines.push("## Top Hotspots");
|
|
6445
|
+
for (const hotspot of report.hotspots.slice(0, 3)) {
|
|
6446
|
+
lines.push(`- \`${hotspot.target}\` (score: \`${hotspot.score}\`)`);
|
|
6447
|
+
}
|
|
6448
|
+
return lines.join("\n");
|
|
6449
|
+
};
|
|
6032
6450
|
var riskProfileOption = () => new Option(
|
|
6033
6451
|
"--risk-profile <profile>",
|
|
6034
6452
|
"risk profile: default (balanced) or personal (down-weights single-maintainer ownership penalties)"
|
|
@@ -6169,6 +6587,220 @@ program.command("report").argument("[path]", "path to the project to analyze").a
|
|
|
6169
6587
|
}
|
|
6170
6588
|
}
|
|
6171
6589
|
);
|
|
6590
|
+
program.command("run").argument("[path]", "path to the project to analyze").addOption(riskProfileOption()).addOption(
|
|
6591
|
+
new Option(
|
|
6592
|
+
"--author-identity <mode>",
|
|
6593
|
+
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
6594
|
+
).choices(["likely_merge", "strict_email"]).default("likely_merge")
|
|
6595
|
+
).addOption(
|
|
6596
|
+
new Option(
|
|
6597
|
+
"--log-level <level>",
|
|
6598
|
+
"log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
|
|
6599
|
+
).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
|
|
6600
|
+
).addOption(
|
|
6601
|
+
new Option("--format <mode>", "combined output format: text, md, json").choices(["text", "md", "json"]).default("text")
|
|
6602
|
+
).addOption(
|
|
6603
|
+
new Option("--detail <level>", "run detail level: compact (default), standard, full").choices(["compact", "standard", "full"]).default("compact")
|
|
6604
|
+
).option("--file <path>", "explain a specific file target").option("--module <name>", "explain a specific module target").option("--top <count>", "number of top hotspots to explain when no target is selected", "5").option("--compare <baseline>", "compare against a baseline snapshot JSON file").option("--snapshot <path>", "write current snapshot JSON artifact").option("--no-trace", "disable trace embedding in generated snapshot").addOption(
|
|
6605
|
+
new Option(
|
|
6606
|
+
"--recent-window-days <days>",
|
|
6607
|
+
"git recency window in days used for evolution volatility metrics"
|
|
6608
|
+
).argParser(parseRecentWindowDays).default(30)
|
|
6609
|
+
).action(
|
|
6610
|
+
async (path, options) => {
|
|
6611
|
+
const logger = createStderrLogger(options.logLevel);
|
|
6612
|
+
const top = Number.parseInt(options.top, 10);
|
|
6613
|
+
const explain = await runExplainCommand(
|
|
6614
|
+
path,
|
|
6615
|
+
options.authorIdentity,
|
|
6616
|
+
{
|
|
6617
|
+
...options.file === void 0 ? {} : { file: options.file },
|
|
6618
|
+
...options.module === void 0 ? {} : { module: options.module },
|
|
6619
|
+
top: Number.isFinite(top) ? top : 5,
|
|
6620
|
+
format: options.format,
|
|
6621
|
+
recentWindowDays: options.recentWindowDays,
|
|
6622
|
+
riskProfile: options.riskProfile
|
|
6623
|
+
},
|
|
6624
|
+
logger
|
|
6625
|
+
);
|
|
6626
|
+
const snapshot = createSnapshot({
|
|
6627
|
+
analysis: explain.summary,
|
|
6628
|
+
...options.trace === true ? { trace: explain.trace } : {}
|
|
6629
|
+
});
|
|
6630
|
+
if (options.snapshot !== void 0) {
|
|
6631
|
+
await writeFile5(options.snapshot, JSON.stringify(snapshot, null, 2), "utf8");
|
|
6632
|
+
logger.info(`snapshot written: ${options.snapshot}`);
|
|
6633
|
+
}
|
|
6634
|
+
const report = options.compare === void 0 ? createReport(snapshot) : createReport(
|
|
6635
|
+
snapshot,
|
|
6636
|
+
compareSnapshots(snapshot, parseSnapshot(await readFile6(options.compare, "utf8")))
|
|
6637
|
+
);
|
|
6638
|
+
if (options.format === "json") {
|
|
6639
|
+
const analyzeSummaryOutput = formatAnalyzeOutput(explain.summary, "summary");
|
|
6640
|
+
if (options.detail === "compact") {
|
|
6641
|
+
process.stdout.write(
|
|
6642
|
+
`${JSON.stringify(
|
|
6643
|
+
{
|
|
6644
|
+
schemaVersion: "codesentinel.run.v1",
|
|
6645
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6646
|
+
repository: report.repository,
|
|
6647
|
+
keyDrivers: extractSummaryValue(
|
|
6648
|
+
extractExplainTextSummary(formatExplainOutput(explain, "text")),
|
|
6649
|
+
"key drivers"
|
|
6650
|
+
),
|
|
6651
|
+
priorityActions: extractSummaryValue(
|
|
6652
|
+
extractExplainTextSummary(formatExplainOutput(explain, "text")),
|
|
6653
|
+
"priority actions"
|
|
6654
|
+
),
|
|
6655
|
+
topHotspots: report.hotspots.slice(0, 3).map((hotspot) => ({
|
|
6656
|
+
target: hotspot.target,
|
|
6657
|
+
score: hotspot.score
|
|
6658
|
+
}))
|
|
6659
|
+
},
|
|
6660
|
+
null,
|
|
6661
|
+
2
|
|
6662
|
+
)}
|
|
6663
|
+
`
|
|
6664
|
+
);
|
|
6665
|
+
return;
|
|
6666
|
+
}
|
|
6667
|
+
if (options.detail === "standard") {
|
|
6668
|
+
const analyzeSummaryPayload = JSON.parse(analyzeSummaryOutput);
|
|
6669
|
+
process.stdout.write(
|
|
6670
|
+
`${JSON.stringify(
|
|
6671
|
+
{
|
|
6672
|
+
schemaVersion: "codesentinel.run.v1",
|
|
6673
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6674
|
+
analyze: analyzeSummaryPayload,
|
|
6675
|
+
explain: {
|
|
6676
|
+
summary: extractExplainTextSummary(formatExplainOutput(explain, "text"))
|
|
6677
|
+
},
|
|
6678
|
+
report: {
|
|
6679
|
+
repository: report.repository,
|
|
6680
|
+
quality: report.quality,
|
|
6681
|
+
hotspots: report.hotspots.slice(0, 5),
|
|
6682
|
+
structural: report.structural,
|
|
6683
|
+
external: report.external
|
|
6684
|
+
}
|
|
6685
|
+
},
|
|
6686
|
+
null,
|
|
6687
|
+
2
|
|
6688
|
+
)}
|
|
6689
|
+
`
|
|
6690
|
+
);
|
|
6691
|
+
return;
|
|
6692
|
+
}
|
|
6693
|
+
process.stdout.write(
|
|
6694
|
+
`${JSON.stringify(
|
|
6695
|
+
{
|
|
6696
|
+
schemaVersion: "codesentinel.run.v1",
|
|
6697
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6698
|
+
analyze: explain.summary,
|
|
6699
|
+
explain,
|
|
6700
|
+
report
|
|
6701
|
+
},
|
|
6702
|
+
null,
|
|
6703
|
+
2
|
|
6704
|
+
)}
|
|
6705
|
+
`
|
|
6706
|
+
);
|
|
6707
|
+
return;
|
|
6708
|
+
}
|
|
6709
|
+
const analyzeRendered = formatAnalyzeOutput(explain.summary, "summary");
|
|
6710
|
+
const explainRendered = formatExplainOutput(explain, options.format);
|
|
6711
|
+
if (options.detail === "compact") {
|
|
6712
|
+
if (options.format === "md") {
|
|
6713
|
+
process.stdout.write(
|
|
6714
|
+
`${renderCompactMarkdown(
|
|
6715
|
+
report,
|
|
6716
|
+
extractExplainMarkdownSummary(formatExplainOutput(explain, "md"))
|
|
6717
|
+
)}
|
|
6718
|
+
`
|
|
6719
|
+
);
|
|
6720
|
+
return;
|
|
6721
|
+
}
|
|
6722
|
+
process.stdout.write(
|
|
6723
|
+
`${renderCompactText(report, extractExplainTextSummary(formatExplainOutput(explain, "text")))}
|
|
6724
|
+
`
|
|
6725
|
+
);
|
|
6726
|
+
return;
|
|
6727
|
+
}
|
|
6728
|
+
if (options.detail === "standard") {
|
|
6729
|
+
if (options.format === "md") {
|
|
6730
|
+
process.stdout.write(
|
|
6731
|
+
`# CodeSentinel Run
|
|
6732
|
+
|
|
6733
|
+
## Analyze
|
|
6734
|
+
\`\`\`json
|
|
6735
|
+
${analyzeRendered}
|
|
6736
|
+
\`\`\`
|
|
6737
|
+
|
|
6738
|
+
## Explain
|
|
6739
|
+
${extractExplainMarkdownSummary(
|
|
6740
|
+
formatExplainOutput(explain, "md")
|
|
6741
|
+
)}
|
|
6742
|
+
|
|
6743
|
+
${renderReportHighlightsMarkdown(report)}
|
|
6744
|
+
`
|
|
6745
|
+
);
|
|
6746
|
+
return;
|
|
6747
|
+
}
|
|
6748
|
+
process.stdout.write(
|
|
6749
|
+
`CodeSentinel Run
|
|
6750
|
+
|
|
6751
|
+
Analyze
|
|
6752
|
+
${analyzeRendered}
|
|
6753
|
+
|
|
6754
|
+
Explain
|
|
6755
|
+
${extractExplainTextSummary(
|
|
6756
|
+
formatExplainOutput(explain, "text")
|
|
6757
|
+
)}
|
|
6758
|
+
|
|
6759
|
+
Report
|
|
6760
|
+
${renderReportHighlightsText(report)}
|
|
6761
|
+
`
|
|
6762
|
+
);
|
|
6763
|
+
return;
|
|
6764
|
+
}
|
|
6765
|
+
const reportRendered = formatReport(report, options.format);
|
|
6766
|
+
if (options.format === "md") {
|
|
6767
|
+
const explainSection = stripLeadingMarkdownHeading(
|
|
6768
|
+
explainRendered,
|
|
6769
|
+
"# CodeSentinel Explanation"
|
|
6770
|
+
);
|
|
6771
|
+
const reportSection = stripLeadingMarkdownHeading(reportRendered, "# CodeSentinel Report");
|
|
6772
|
+
process.stdout.write(
|
|
6773
|
+
`# CodeSentinel Run
|
|
6774
|
+
|
|
6775
|
+
## Analyze
|
|
6776
|
+
\`\`\`json
|
|
6777
|
+
${analyzeRendered}
|
|
6778
|
+
\`\`\`
|
|
6779
|
+
|
|
6780
|
+
## Explain
|
|
6781
|
+
${explainSection}
|
|
6782
|
+
|
|
6783
|
+
## Report
|
|
6784
|
+
${reportSection}
|
|
6785
|
+
`
|
|
6786
|
+
);
|
|
6787
|
+
return;
|
|
6788
|
+
}
|
|
6789
|
+
process.stdout.write(
|
|
6790
|
+
`CodeSentinel Run
|
|
6791
|
+
|
|
6792
|
+
Analyze
|
|
6793
|
+
${analyzeRendered}
|
|
6794
|
+
|
|
6795
|
+
Explain
|
|
6796
|
+
${explainRendered}
|
|
6797
|
+
|
|
6798
|
+
Report
|
|
6799
|
+
${reportRendered}
|
|
6800
|
+
`
|
|
6801
|
+
);
|
|
6802
|
+
}
|
|
6803
|
+
);
|
|
6172
6804
|
var parseGateNumber = (value, optionName) => {
|
|
6173
6805
|
if (value === void 0) {
|
|
6174
6806
|
return void 0;
|