@getcodesentinel/codesentinel 1.19.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -1753,6 +1753,7 @@ var analyzeDependencyCandidateFromRegistry = async (input) => {
|
|
|
1753
1753
|
};
|
|
1754
1754
|
|
|
1755
1755
|
// ../reporter/dist/index.js
|
|
1756
|
+
import { basename, posix } from "path";
|
|
1756
1757
|
var SNAPSHOT_SCHEMA_VERSION = "codesentinel.snapshot.v1";
|
|
1757
1758
|
var REPORT_SCHEMA_VERSION = "codesentinel.report.v1";
|
|
1758
1759
|
var RISK_MODEL_VERSION = "deterministic-v1";
|
|
@@ -1914,6 +1915,11 @@ var compareSnapshots = (current, baseline) => {
|
|
|
1914
1915
|
}
|
|
1915
1916
|
};
|
|
1916
1917
|
};
|
|
1918
|
+
var toPosixDirname = (value) => {
|
|
1919
|
+
const normalized = value.replaceAll("\\", "/");
|
|
1920
|
+
const directory = posix.dirname(normalized);
|
|
1921
|
+
return directory === "." ? "root" : directory;
|
|
1922
|
+
};
|
|
1917
1923
|
var findTraceTarget = (snapshot, targetType, targetId) => snapshot.trace?.targets.find(
|
|
1918
1924
|
(target) => target.targetType === targetType && target.targetId === targetId
|
|
1919
1925
|
);
|
|
@@ -1959,14 +1965,27 @@ var suggestedActions = (target) => {
|
|
|
1959
1965
|
}
|
|
1960
1966
|
return [...new Set(actions)].slice(0, 3);
|
|
1961
1967
|
};
|
|
1962
|
-
var
|
|
1968
|
+
var hotspotReason = (factors) => {
|
|
1969
|
+
if (factors.length === 0) {
|
|
1970
|
+
return "Limited trace data available for this hotspot.";
|
|
1971
|
+
}
|
|
1972
|
+
return factors.slice(0, 2).map((factor) => `${factor.label} (${factor.contribution})`).join(" + ");
|
|
1973
|
+
};
|
|
1974
|
+
var hotspotItems = (snapshot) => snapshot.analysis.risk.hotspots.slice(0, 10).map((hotspot, index) => {
|
|
1963
1975
|
const fileScore = snapshot.analysis.risk.fileScores.find((item) => item.file === hotspot.file);
|
|
1976
|
+
const evolutionMetrics = snapshot.analysis.evolution.available ? snapshot.analysis.evolution.files.find((item) => item.filePath === hotspot.file) : void 0;
|
|
1964
1977
|
const traceTarget = findTraceTarget(snapshot, "file", hotspot.file);
|
|
1965
1978
|
const factors = toRenderedFactors(traceTarget);
|
|
1966
1979
|
return {
|
|
1980
|
+
rank: index + 1,
|
|
1967
1981
|
target: hotspot.file,
|
|
1982
|
+
module: toPosixDirname(hotspot.file),
|
|
1968
1983
|
score: hotspot.score,
|
|
1969
1984
|
normalizedScore: fileScore?.normalizedScore ?? round43(hotspot.score / 100),
|
|
1985
|
+
commitCount: evolutionMetrics?.commitCount ?? null,
|
|
1986
|
+
churnTotal: evolutionMetrics?.churnTotal ?? null,
|
|
1987
|
+
riskContributions: hotspot.factors,
|
|
1988
|
+
reason: hotspotReason(factors),
|
|
1970
1989
|
topFactors: factors,
|
|
1971
1990
|
suggestedActions: suggestedActions(traceTarget),
|
|
1972
1991
|
biggestLevers: (traceTarget?.reductionLevers ?? []).slice(0, 3).map((lever) => `${factorLabel(lever.factorId)} (${lever.estimatedImpact})`)
|
|
@@ -1987,6 +2006,51 @@ var repositoryConfidence = (snapshot) => {
|
|
|
1987
2006
|
);
|
|
1988
2007
|
return round43(weighted / weight);
|
|
1989
2008
|
};
|
|
2009
|
+
var normalizeDependencyScope = (scope) => {
|
|
2010
|
+
switch (scope) {
|
|
2011
|
+
case "prod":
|
|
2012
|
+
case "dev":
|
|
2013
|
+
return scope;
|
|
2014
|
+
default:
|
|
2015
|
+
return "unknown";
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
var topStructuralFiles = (snapshot, selector) => [...snapshot.analysis.structural.files].sort((a, b) => selector(b) - selector(a) || a.relativePath.localeCompare(b.relativePath)).slice(0, 5).map((file) => ({
|
|
2019
|
+
file: file.relativePath,
|
|
2020
|
+
module: toPosixDirname(file.relativePath),
|
|
2021
|
+
value: selector(file)
|
|
2022
|
+
}));
|
|
2023
|
+
var cycleDetails = (snapshot) => snapshot.analysis.structural.cycles.map((cycle, index) => {
|
|
2024
|
+
const nodes = [...cycle.nodes].sort((a, b) => a.localeCompare(b));
|
|
2025
|
+
return {
|
|
2026
|
+
id: `cycle-${index + 1}`,
|
|
2027
|
+
size: nodes.length,
|
|
2028
|
+
nodes,
|
|
2029
|
+
path: nodes.join(" -> ")
|
|
2030
|
+
};
|
|
2031
|
+
});
|
|
2032
|
+
var riskyDependencies = (snapshot) => {
|
|
2033
|
+
if (!snapshot.analysis.external.available) {
|
|
2034
|
+
return [];
|
|
2035
|
+
}
|
|
2036
|
+
const dependencyByName = new Map(
|
|
2037
|
+
snapshot.analysis.external.dependencies.map((dependency) => [dependency.name, dependency])
|
|
2038
|
+
);
|
|
2039
|
+
return snapshot.analysis.risk.dependencyScores.map((score) => {
|
|
2040
|
+
const dependency = dependencyByName.get(score.dependency);
|
|
2041
|
+
const riskSignals = [.../* @__PURE__ */ new Set([...score.ownRiskSignals, ...score.inheritedRiskSignals])];
|
|
2042
|
+
return {
|
|
2043
|
+
name: score.dependency,
|
|
2044
|
+
score: score.score,
|
|
2045
|
+
normalizedScore: score.normalizedScore,
|
|
2046
|
+
dependencyScope: normalizeDependencyScope(dependency?.dependencyScope),
|
|
2047
|
+
direct: dependency?.direct ?? false,
|
|
2048
|
+
resolvedVersion: dependency?.resolvedVersion ?? null,
|
|
2049
|
+
riskSignals,
|
|
2050
|
+
reason: riskSignals.length === 0 ? "Derived from aggregate dependency risk signals." : riskSignals.join(", ")
|
|
2051
|
+
};
|
|
2052
|
+
}).filter((dependency) => dependency.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, 20);
|
|
2053
|
+
};
|
|
1990
2054
|
var repositoryDimensionScores = (snapshot) => {
|
|
1991
2055
|
const target = findTraceTarget(snapshot, "repository", snapshot.analysis.structural.targetPath);
|
|
1992
2056
|
if (target === void 0) {
|
|
@@ -2019,6 +2083,7 @@ var createReport = (snapshot, diff) => {
|
|
|
2019
2083
|
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
2020
2084
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2021
2085
|
repository: {
|
|
2086
|
+
name: basename(snapshot.analysis.structural.targetPath) || snapshot.analysis.structural.targetPath,
|
|
2022
2087
|
targetPath: snapshot.analysis.structural.targetPath,
|
|
2023
2088
|
riskScore: snapshot.analysis.risk.riskScore,
|
|
2024
2089
|
normalizedScore: snapshot.analysis.risk.normalizedScore,
|
|
@@ -2034,6 +2099,12 @@ var createReport = (snapshot, diff) => {
|
|
|
2034
2099
|
cycles: snapshot.analysis.structural.cycles.map(
|
|
2035
2100
|
(cycle) => [...cycle.nodes].sort((a, b) => a.localeCompare(b)).join(" -> ")
|
|
2036
2101
|
),
|
|
2102
|
+
cycleDetails: cycleDetails(snapshot),
|
|
2103
|
+
fanInOutExtremes: {
|
|
2104
|
+
highestFanIn: topStructuralFiles(snapshot, (file) => file.fanIn),
|
|
2105
|
+
highestFanOut: topStructuralFiles(snapshot, (file) => file.fanOut),
|
|
2106
|
+
deepestFiles: topStructuralFiles(snapshot, (file) => file.depth)
|
|
2107
|
+
},
|
|
2037
2108
|
fragileClusters: snapshot.analysis.risk.fragileClusters.map((cluster) => ({
|
|
2038
2109
|
id: cluster.id,
|
|
2039
2110
|
kind: cluster.kind,
|
|
@@ -2057,7 +2128,8 @@ var createReport = (snapshot, diff) => {
|
|
|
2057
2128
|
),
|
|
2058
2129
|
abandonedDependencies: [...external.abandonedDependencies].sort(
|
|
2059
2130
|
(a, b) => a.localeCompare(b)
|
|
2060
|
-
)
|
|
2131
|
+
),
|
|
2132
|
+
riskyDependencies: riskyDependencies(snapshot)
|
|
2061
2133
|
},
|
|
2062
2134
|
appendix: {
|
|
2063
2135
|
snapshotSchemaVersion: snapshot.schemaVersion,
|
|
@@ -2298,7 +2370,7 @@ var formatReport = (report, format) => {
|
|
|
2298
2370
|
|
|
2299
2371
|
// ../governance/dist/index.js
|
|
2300
2372
|
import { mkdirSync, rmSync } from "fs";
|
|
2301
|
-
import { basename, join as join5, resolve } from "path";
|
|
2373
|
+
import { basename as basename2, join as join5, resolve } from "path";
|
|
2302
2374
|
import { execFile } from "child_process";
|
|
2303
2375
|
import { promisify } from "util";
|
|
2304
2376
|
var EXIT_CODES = {
|
|
@@ -2911,9 +2983,9 @@ var resolveAutoBaselineRef = async (input) => {
|
|
|
2911
2983
|
|
|
2912
2984
|
// src/index.ts
|
|
2913
2985
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2914
|
-
import { readFile as
|
|
2915
|
-
import { dirname as
|
|
2916
|
-
import { fileURLToPath } from "url";
|
|
2986
|
+
import { readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
|
|
2987
|
+
import { dirname as dirname3, resolve as resolve6 } from "path";
|
|
2988
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2917
2989
|
|
|
2918
2990
|
// src/application/format-analyze-output.ts
|
|
2919
2991
|
var toHealthTier2 = (score) => {
|
|
@@ -3558,7 +3630,7 @@ var promptInstall = async (packageName, latestVersion, currentVersion) => {
|
|
|
3558
3630
|
);
|
|
3559
3631
|
return "skip";
|
|
3560
3632
|
}
|
|
3561
|
-
return await new Promise((
|
|
3633
|
+
return await new Promise((resolve7) => {
|
|
3562
3634
|
emitKeypressEvents(stdin);
|
|
3563
3635
|
let selectedIndex = 0;
|
|
3564
3636
|
const previousRawMode = stdin.isRaw;
|
|
@@ -3583,7 +3655,7 @@ var promptInstall = async (packageName, latestVersion, currentVersion) => {
|
|
|
3583
3655
|
} else {
|
|
3584
3656
|
stderr.write("\n");
|
|
3585
3657
|
}
|
|
3586
|
-
|
|
3658
|
+
resolve7(choice);
|
|
3587
3659
|
};
|
|
3588
3660
|
const onKeypress = (_str, key) => {
|
|
3589
3661
|
if (key.ctrl === true && key.name === "c") {
|
|
@@ -3777,7 +3849,7 @@ var promptSelection = async (currentVersion, actions) => {
|
|
|
3777
3849
|
if (!stdin2.isTTY || !stderr2.isTTY || typeof stdin2.setRawMode !== "function") {
|
|
3778
3850
|
return "exit";
|
|
3779
3851
|
}
|
|
3780
|
-
return await new Promise((
|
|
3852
|
+
return await new Promise((resolve7) => {
|
|
3781
3853
|
emitKeypressEvents2(stdin2);
|
|
3782
3854
|
let selectedIndex = 0;
|
|
3783
3855
|
const previousRawMode = stdin2.isRaw;
|
|
@@ -3792,7 +3864,7 @@ var promptSelection = async (currentVersion, actions) => {
|
|
|
3792
3864
|
stdin2.setRawMode(previousRawMode);
|
|
3793
3865
|
clearTerminal();
|
|
3794
3866
|
showCursor2();
|
|
3795
|
-
|
|
3867
|
+
resolve7(selection);
|
|
3796
3868
|
};
|
|
3797
3869
|
const onKeypress = (_str, key) => {
|
|
3798
3870
|
if (key.ctrl === true && key.name === "c") {
|
|
@@ -3885,7 +3957,7 @@ var waitForReturnToMenu = async () => {
|
|
|
3885
3957
|
}
|
|
3886
3958
|
stderr2.write(`
|
|
3887
3959
|
${PROMPT_PADDING}Press enter to return to the menu...`);
|
|
3888
|
-
await new Promise((
|
|
3960
|
+
await new Promise((resolve7) => {
|
|
3889
3961
|
emitKeypressEvents2(stdin2);
|
|
3890
3962
|
const previousRawMode = stdin2.isRaw;
|
|
3891
3963
|
const cleanup = () => {
|
|
@@ -3894,7 +3966,7 @@ ${PROMPT_PADDING}Press enter to return to the menu...`);
|
|
|
3894
3966
|
stdin2.setRawMode(previousRawMode);
|
|
3895
3967
|
showCursor2();
|
|
3896
3968
|
stderr2.write("\n");
|
|
3897
|
-
|
|
3969
|
+
resolve7();
|
|
3898
3970
|
};
|
|
3899
3971
|
const onKeypress = (_str, key) => {
|
|
3900
3972
|
if (key.ctrl === true && key.name === "c") {
|
|
@@ -3912,7 +3984,7 @@ ${PROMPT_PADDING}Press enter to return to the menu...`);
|
|
|
3912
3984
|
});
|
|
3913
3985
|
};
|
|
3914
3986
|
var runCliCommand = async (scriptPath2, args) => {
|
|
3915
|
-
return await new Promise((
|
|
3987
|
+
return await new Promise((resolve7, reject) => {
|
|
3916
3988
|
const child = spawn2(process.execPath, [...process.execArgv, scriptPath2, ...args], {
|
|
3917
3989
|
stdio: ["inherit", "pipe", "pipe"],
|
|
3918
3990
|
env: {
|
|
@@ -3926,7 +3998,7 @@ var runCliCommand = async (scriptPath2, args) => {
|
|
|
3926
3998
|
reject(error);
|
|
3927
3999
|
});
|
|
3928
4000
|
child.on("close", (code) => {
|
|
3929
|
-
|
|
4001
|
+
resolve7(code ?? 1);
|
|
3930
4002
|
});
|
|
3931
4003
|
});
|
|
3932
4004
|
};
|
|
@@ -4683,6 +4755,26 @@ var finalizeAuthorDistribution = (authorCommits) => {
|
|
|
4683
4755
|
share: round44(commits / totalCommits)
|
|
4684
4756
|
})).sort((a, b) => b.commits - a.commits || a.authorId.localeCompare(b.authorId));
|
|
4685
4757
|
};
|
|
4758
|
+
var finalizeAuthorChurnDistribution = (authorChurn) => {
|
|
4759
|
+
const entries = [...authorChurn.entries()].map(([authorId, churn]) => {
|
|
4760
|
+
const churnAdded = churn.churnAdded;
|
|
4761
|
+
const churnDeleted = churn.churnDeleted;
|
|
4762
|
+
return {
|
|
4763
|
+
authorId,
|
|
4764
|
+
churnAdded,
|
|
4765
|
+
churnDeleted,
|
|
4766
|
+
churnTotal: churnAdded + churnDeleted
|
|
4767
|
+
};
|
|
4768
|
+
});
|
|
4769
|
+
const totalChurn = entries.reduce((sum, entry) => sum + entry.churnTotal, 0);
|
|
4770
|
+
if (totalChurn === 0) {
|
|
4771
|
+
return [];
|
|
4772
|
+
}
|
|
4773
|
+
return entries.map((entry) => ({
|
|
4774
|
+
...entry,
|
|
4775
|
+
share: round44(entry.churnTotal / totalChurn)
|
|
4776
|
+
})).sort((a, b) => b.churnTotal - a.churnTotal || a.authorId.localeCompare(b.authorId));
|
|
4777
|
+
};
|
|
4686
4778
|
var buildCouplingMatrix = (coChangeByPair, fileCommitCount, consideredCommits, skippedLargeCommits, maxCouplingPairs) => {
|
|
4687
4779
|
const allPairs = [];
|
|
4688
4780
|
for (const [key, coChangeCommits] of coChangeByPair.entries()) {
|
|
@@ -4751,10 +4843,19 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
|
|
|
4751
4843
|
recentCommitCount: 0,
|
|
4752
4844
|
churnAdded: 0,
|
|
4753
4845
|
churnDeleted: 0,
|
|
4754
|
-
|
|
4846
|
+
authorsByCommits: /* @__PURE__ */ new Map(),
|
|
4847
|
+
authorsByChurn: /* @__PURE__ */ new Map()
|
|
4755
4848
|
};
|
|
4756
4849
|
current.churnAdded += fileChange.additions;
|
|
4757
4850
|
current.churnDeleted += fileChange.deletions;
|
|
4851
|
+
const effectiveAuthorId = authorAliasById.get(commit.authorId) ?? commit.authorId;
|
|
4852
|
+
const authorChurn = current.authorsByChurn.get(effectiveAuthorId) ?? {
|
|
4853
|
+
churnAdded: 0,
|
|
4854
|
+
churnDeleted: 0
|
|
4855
|
+
};
|
|
4856
|
+
authorChurn.churnAdded += fileChange.additions;
|
|
4857
|
+
authorChurn.churnDeleted += fileChange.deletions;
|
|
4858
|
+
current.authorsByChurn.set(effectiveAuthorId, authorChurn);
|
|
4758
4859
|
fileStats.set(fileChange.filePath, current);
|
|
4759
4860
|
}
|
|
4760
4861
|
for (const filePath of uniqueFiles) {
|
|
@@ -4767,7 +4868,10 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
|
|
|
4767
4868
|
current.recentCommitCount += 1;
|
|
4768
4869
|
}
|
|
4769
4870
|
const effectiveAuthorId = authorAliasById.get(commit.authorId) ?? commit.authorId;
|
|
4770
|
-
current.
|
|
4871
|
+
current.authorsByCommits.set(
|
|
4872
|
+
effectiveAuthorId,
|
|
4873
|
+
(current.authorsByCommits.get(effectiveAuthorId) ?? 0) + 1
|
|
4874
|
+
);
|
|
4771
4875
|
}
|
|
4772
4876
|
const orderedFiles = [...uniqueFiles].sort((a, b) => a.localeCompare(b));
|
|
4773
4877
|
if (orderedFiles.length > 1) {
|
|
@@ -4790,8 +4894,10 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
|
|
|
4790
4894
|
}
|
|
4791
4895
|
}
|
|
4792
4896
|
const files = [...fileStats.entries()].map(([filePath, stats]) => {
|
|
4793
|
-
const
|
|
4794
|
-
const
|
|
4897
|
+
const authorDistributionByCommits = finalizeAuthorDistribution(stats.authorsByCommits);
|
|
4898
|
+
const authorDistributionByChurn = finalizeAuthorChurnDistribution(stats.authorsByChurn);
|
|
4899
|
+
const topAuthorShareByCommits = authorDistributionByCommits[0]?.share ?? 0;
|
|
4900
|
+
const topAuthorShareByChurn = authorDistributionByChurn[0]?.share ?? 0;
|
|
4795
4901
|
return {
|
|
4796
4902
|
filePath,
|
|
4797
4903
|
commitCount: stats.commitCount,
|
|
@@ -4801,9 +4907,18 @@ var computeRepositoryEvolutionSummary = (targetPath, commits, config) => {
|
|
|
4801
4907
|
churnTotal: stats.churnAdded + stats.churnDeleted,
|
|
4802
4908
|
recentCommitCount: stats.recentCommitCount,
|
|
4803
4909
|
recentVolatility: stats.commitCount === 0 ? 0 : round44(stats.recentCommitCount / stats.commitCount),
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4910
|
+
topAuthorShareByCommits,
|
|
4911
|
+
busFactorByCommits: computeBusFactor(
|
|
4912
|
+
authorDistributionByCommits,
|
|
4913
|
+
config.busFactorCoverageThreshold
|
|
4914
|
+
),
|
|
4915
|
+
authorDistributionByCommits,
|
|
4916
|
+
topAuthorShareByChurn,
|
|
4917
|
+
busFactorByChurn: computeBusFactor(
|
|
4918
|
+
authorDistributionByChurn,
|
|
4919
|
+
config.busFactorCoverageThreshold
|
|
4920
|
+
),
|
|
4921
|
+
authorDistributionByChurn
|
|
4807
4922
|
};
|
|
4808
4923
|
}).sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
4809
4924
|
const fileCommitCount = new Map(files.map((file) => [file.filePath, file.commitCount]));
|
|
@@ -5558,15 +5673,15 @@ var computeRepositoryHealthSummary = (input) => {
|
|
|
5558
5673
|
let singleContributorFiles = 0;
|
|
5559
5674
|
let trackedFiles = 0;
|
|
5560
5675
|
for (const file of evolutionSourceFiles) {
|
|
5561
|
-
if (file.commitCount <= 0 || file.
|
|
5676
|
+
if (file.commitCount <= 0 || file.authorDistributionByCommits.length === 0) {
|
|
5562
5677
|
continue;
|
|
5563
5678
|
}
|
|
5564
5679
|
trackedFiles += 1;
|
|
5565
|
-
const dominantShare = clamp01(file.
|
|
5566
|
-
if (file.
|
|
5680
|
+
const dominantShare = clamp01(file.authorDistributionByCommits[0]?.share ?? 0);
|
|
5681
|
+
if (file.authorDistributionByCommits.length === 1 || dominantShare >= 0.9) {
|
|
5567
5682
|
singleContributorFiles += 1;
|
|
5568
5683
|
}
|
|
5569
|
-
for (const author of file.
|
|
5684
|
+
for (const author of file.authorDistributionByCommits) {
|
|
5570
5685
|
const commits = Math.max(0, author.commits);
|
|
5571
5686
|
if (commits <= 0) {
|
|
5572
5687
|
continue;
|
|
@@ -6329,7 +6444,7 @@ var computeEvolutionScales = (evolutionByFile, config) => {
|
|
|
6329
6444
|
config.quantileClamp.upper
|
|
6330
6445
|
),
|
|
6331
6446
|
busFactor: buildQuantileScale(
|
|
6332
|
-
evolutionFiles.map((metrics) => metrics.
|
|
6447
|
+
evolutionFiles.map((metrics) => metrics.busFactorByCommits),
|
|
6333
6448
|
config.quantileClamp.lower,
|
|
6334
6449
|
config.quantileClamp.upper
|
|
6335
6450
|
)
|
|
@@ -6518,9 +6633,9 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
6518
6633
|
evolutionScales.churnTotal
|
|
6519
6634
|
);
|
|
6520
6635
|
volatilityRisk = toUnitInterval(evolutionMetrics.recentVolatility);
|
|
6521
|
-
ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.
|
|
6636
|
+
ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShareByCommits);
|
|
6522
6637
|
busFactorRisk = toUnitInterval(
|
|
6523
|
-
1 - normalizeWithScale(evolutionMetrics.
|
|
6638
|
+
1 - normalizeWithScale(evolutionMetrics.busFactorByCommits, evolutionScales.busFactor)
|
|
6524
6639
|
);
|
|
6525
6640
|
const evolutionWeights = config.evolutionFactorWeights;
|
|
6526
6641
|
evolutionFactor = toUnitInterval(
|
|
@@ -6577,8 +6692,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
6577
6692
|
commitCount: evolutionMetrics?.commitCount ?? null,
|
|
6578
6693
|
churnTotal: evolutionMetrics?.churnTotal ?? null,
|
|
6579
6694
|
recentVolatility: evolutionMetrics?.recentVolatility ?? null,
|
|
6580
|
-
|
|
6581
|
-
|
|
6695
|
+
topAuthorShareByCommits: evolutionMetrics?.topAuthorShareByCommits ?? null,
|
|
6696
|
+
busFactorByCommits: evolutionMetrics?.busFactorByCommits ?? null,
|
|
6582
6697
|
dependencyAffinity: round46(dependencyAffinity),
|
|
6583
6698
|
repositoryExternalPressure: round46(dependencyComputation.repositoryExternalPressure),
|
|
6584
6699
|
structuralAttenuation: round46(structuralAttenuation)
|
|
@@ -6646,8 +6761,8 @@ var computeRiskSummary = (structural, evolution, external, config, traceCollecto
|
|
|
6646
6761
|
commitCount: context.rawMetrics.commitCount,
|
|
6647
6762
|
churnTotal: context.rawMetrics.churnTotal,
|
|
6648
6763
|
recentVolatility: context.rawMetrics.recentVolatility,
|
|
6649
|
-
|
|
6650
|
-
|
|
6764
|
+
topAuthorShareByCommits: context.rawMetrics.topAuthorShareByCommits,
|
|
6765
|
+
busFactorByCommits: context.rawMetrics.busFactorByCommits
|
|
6651
6766
|
},
|
|
6652
6767
|
normalizedMetrics: {
|
|
6653
6768
|
frequencyRisk: context.normalizedMetrics.frequencyRisk,
|
|
@@ -7597,7 +7712,114 @@ ${ciMarkdown}`;
|
|
|
7597
7712
|
};
|
|
7598
7713
|
|
|
7599
7714
|
// src/application/run-report-command.ts
|
|
7600
|
-
import {
|
|
7715
|
+
import { join as join8 } from "path";
|
|
7716
|
+
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
7717
|
+
|
|
7718
|
+
// src/application/html-report.ts
|
|
7719
|
+
import { mkdir as mkdir3, readFile as readFile5, rm, stat as stat2, writeFile as writeFile5 } from "fs/promises";
|
|
7720
|
+
import { dirname as dirname2, join as join7, resolve as resolve5 } from "path";
|
|
7721
|
+
import { fileURLToPath } from "url";
|
|
7722
|
+
var HTML_REPORT_DIR = ".codesentinel/report";
|
|
7723
|
+
var getBundledHtmlAppPath = () => resolve5(dirname2(fileURLToPath(import.meta.url)), "html-report-app");
|
|
7724
|
+
var serializeReportBootstrap = (report) => `window.__CODESENTINEL_REPORT__ = ${JSON.stringify(report).replaceAll("</", "<\\/")};
|
|
7725
|
+
`;
|
|
7726
|
+
var escapeInlineScript = (script) => script.replaceAll("</script", "<\\/script").replaceAll("<script", "\\x3Cscript").replaceAll("<!--", "\\x3C!--").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
7727
|
+
var ensureDirectory = async (directoryPath) => {
|
|
7728
|
+
await mkdir3(directoryPath, { recursive: true });
|
|
7729
|
+
};
|
|
7730
|
+
var readReferencedAssets = async (appPath, indexHtml) => {
|
|
7731
|
+
const stylesheetPaths = [
|
|
7732
|
+
...indexHtml.matchAll(/<link[^>]+rel=["']stylesheet["'][^>]+href=["']([^"']+)["'][^>]*>/g)
|
|
7733
|
+
].map((match) => match[1]).filter((value) => value !== void 0);
|
|
7734
|
+
const styles = await Promise.all(
|
|
7735
|
+
stylesheetPaths.map(async (href) => readFile5(resolve5(appPath, href), "utf8"))
|
|
7736
|
+
);
|
|
7737
|
+
const scriptPaths = [...indexHtml.matchAll(/<script[^>]+src=["']([^"']+)["'][^>]*><\/script>/g)].map((match) => match[1]).filter((value) => value !== void 0);
|
|
7738
|
+
const scripts = await Promise.all(
|
|
7739
|
+
scriptPaths.map(async (src) => readFile5(resolve5(appPath, src), "utf8"))
|
|
7740
|
+
);
|
|
7741
|
+
return { styles, scripts };
|
|
7742
|
+
};
|
|
7743
|
+
var inlineBuiltHtml = async (appPath, report) => {
|
|
7744
|
+
const indexHtml = await readFile5(resolve5(appPath, "index.html"), "utf8");
|
|
7745
|
+
const { styles, scripts } = await readReferencedAssets(appPath, indexHtml);
|
|
7746
|
+
const htmlWithoutExternalAssets = indexHtml.replace(/\s*<link[^>]+rel=["']stylesheet["'][^>]*>\s*/g, "").replace(/\s*<script[^>]+src=["'][^"']+["'][^>]*><\/script>\s*/g, "");
|
|
7747
|
+
const inlineStyles = styles.map((style) => `<style>
|
|
7748
|
+
${style}
|
|
7749
|
+
</style>`).join("\n");
|
|
7750
|
+
const bootstrapScript = `<script>
|
|
7751
|
+
${serializeReportBootstrap(report)}</script>`;
|
|
7752
|
+
const inlineScripts = scripts.map((script) => `<script>
|
|
7753
|
+
${escapeInlineScript(script)}
|
|
7754
|
+
</script>`).join("\n");
|
|
7755
|
+
return htmlWithoutExternalAssets.replace("</head>", () => `${inlineStyles === "" ? "" : `${inlineStyles}
|
|
7756
|
+
`}</head>`).replace(
|
|
7757
|
+
"</body>",
|
|
7758
|
+
() => `${bootstrapScript}
|
|
7759
|
+
${inlineScripts === "" ? "" : `${inlineScripts}
|
|
7760
|
+
`}</body>`
|
|
7761
|
+
);
|
|
7762
|
+
};
|
|
7763
|
+
var assertHtmlAppAssets = async (assetPath) => {
|
|
7764
|
+
const assetStats = await stat2(assetPath).catch(() => void 0);
|
|
7765
|
+
if (assetStats === void 0 || !assetStats.isDirectory()) {
|
|
7766
|
+
throw new Error(
|
|
7767
|
+
`html_report_assets_missing: expected built app at ${assetPath}. Run the workspace build first.`
|
|
7768
|
+
);
|
|
7769
|
+
}
|
|
7770
|
+
};
|
|
7771
|
+
var resolveHtmlReportOutputPath = (repositoryPath, outputPath) => {
|
|
7772
|
+
const invocationCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
7773
|
+
return resolve5(invocationCwd, outputPath ?? join7(repositoryPath, HTML_REPORT_DIR));
|
|
7774
|
+
};
|
|
7775
|
+
var writeHtmlReportBundle = async (report, options) => {
|
|
7776
|
+
const bundledAppPath = options.bundledAppPath ?? getBundledHtmlAppPath();
|
|
7777
|
+
await assertHtmlAppAssets(bundledAppPath);
|
|
7778
|
+
const outputPath = resolveHtmlReportOutputPath(options.repositoryPath, options.outputPath);
|
|
7779
|
+
await rm(outputPath, { recursive: true, force: true });
|
|
7780
|
+
await ensureDirectory(outputPath);
|
|
7781
|
+
await writeFile5(
|
|
7782
|
+
resolve5(outputPath, "index.html"),
|
|
7783
|
+
await inlineBuiltHtml(bundledAppPath, report),
|
|
7784
|
+
"utf8"
|
|
7785
|
+
);
|
|
7786
|
+
return outputPath;
|
|
7787
|
+
};
|
|
7788
|
+
|
|
7789
|
+
// src/application/open-path.ts
|
|
7790
|
+
import { spawn as spawn3 } from "child_process";
|
|
7791
|
+
import { platform } from "os";
|
|
7792
|
+
var openCommandForPlatform = (targetPath) => {
|
|
7793
|
+
switch (platform()) {
|
|
7794
|
+
case "darwin":
|
|
7795
|
+
return { command: "open", args: [targetPath] };
|
|
7796
|
+
case "win32":
|
|
7797
|
+
return { command: "cmd", args: ["/c", "start", "", targetPath] };
|
|
7798
|
+
case "linux":
|
|
7799
|
+
return { command: "xdg-open", args: [targetPath] };
|
|
7800
|
+
default:
|
|
7801
|
+
return void 0;
|
|
7802
|
+
}
|
|
7803
|
+
};
|
|
7804
|
+
var openPath = async (targetPath) => {
|
|
7805
|
+
const command = openCommandForPlatform(targetPath);
|
|
7806
|
+
if (command === void 0) {
|
|
7807
|
+
return false;
|
|
7808
|
+
}
|
|
7809
|
+
return new Promise((resolve7) => {
|
|
7810
|
+
const child = spawn3(command.command, command.args, {
|
|
7811
|
+
detached: true,
|
|
7812
|
+
stdio: "ignore"
|
|
7813
|
+
});
|
|
7814
|
+
child.once("error", () => resolve7(false));
|
|
7815
|
+
child.once("spawn", () => {
|
|
7816
|
+
child.unref();
|
|
7817
|
+
resolve7(true);
|
|
7818
|
+
});
|
|
7819
|
+
});
|
|
7820
|
+
};
|
|
7821
|
+
|
|
7822
|
+
// src/application/run-report-command.ts
|
|
7601
7823
|
var runReportCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
|
|
7602
7824
|
logger.info("building analysis snapshot");
|
|
7603
7825
|
const current = await buildAnalysisSnapshot(
|
|
@@ -7611,7 +7833,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
|
|
|
7611
7833
|
logger
|
|
7612
7834
|
);
|
|
7613
7835
|
if (options.snapshotPath !== void 0) {
|
|
7614
|
-
await
|
|
7836
|
+
await writeFile6(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
|
|
7615
7837
|
logger.info(`snapshot written: ${options.snapshotPath}`);
|
|
7616
7838
|
}
|
|
7617
7839
|
let report;
|
|
@@ -7619,17 +7841,39 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
|
|
|
7619
7841
|
report = createReport(current);
|
|
7620
7842
|
} else {
|
|
7621
7843
|
logger.info(`loading baseline snapshot: ${options.comparePath}`);
|
|
7622
|
-
const baselineRaw = await
|
|
7844
|
+
const baselineRaw = await readFile6(options.comparePath, "utf8");
|
|
7623
7845
|
const baseline = parseSnapshot(baselineRaw);
|
|
7624
7846
|
const diff = compareSnapshots(current, baseline);
|
|
7625
7847
|
report = createReport(current, diff);
|
|
7626
7848
|
}
|
|
7849
|
+
if (options.format === "html") {
|
|
7850
|
+
const bundlePath = await writeHtmlReportBundle(report, {
|
|
7851
|
+
repositoryPath: current.analysis.structural.targetPath,
|
|
7852
|
+
...options.outputPath === void 0 ? {} : { outputPath: options.outputPath }
|
|
7853
|
+
});
|
|
7854
|
+
if (options.open === true) {
|
|
7855
|
+
const opened = await openPath(join8(bundlePath, "index.html"));
|
|
7856
|
+
if (!opened) {
|
|
7857
|
+
logger.warn("unable to open html report automatically on this platform");
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7860
|
+
logger.info(`html report written: ${bundlePath}`);
|
|
7861
|
+
return {
|
|
7862
|
+
report,
|
|
7863
|
+
rendered: bundlePath,
|
|
7864
|
+
outputPath: bundlePath
|
|
7865
|
+
};
|
|
7866
|
+
}
|
|
7627
7867
|
const rendered = formatReport(report, options.format);
|
|
7628
7868
|
if (options.outputPath !== void 0) {
|
|
7629
|
-
await
|
|
7869
|
+
await writeFile6(options.outputPath, rendered, "utf8");
|
|
7630
7870
|
logger.info(`report written: ${options.outputPath}`);
|
|
7631
7871
|
}
|
|
7632
|
-
return {
|
|
7872
|
+
return {
|
|
7873
|
+
report,
|
|
7874
|
+
rendered,
|
|
7875
|
+
...options.outputPath === void 0 ? {} : { outputPath: options.outputPath }
|
|
7876
|
+
};
|
|
7633
7877
|
};
|
|
7634
7878
|
|
|
7635
7879
|
// src/application/run-explain-command.ts
|
|
@@ -7699,7 +7943,7 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
|
|
|
7699
7943
|
|
|
7700
7944
|
// src/index.ts
|
|
7701
7945
|
var program = new Command();
|
|
7702
|
-
var packageJsonPath =
|
|
7946
|
+
var packageJsonPath = resolve6(dirname3(fileURLToPath2(import.meta.url)), "../package.json");
|
|
7703
7947
|
var { version } = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
7704
7948
|
var parseRecentWindowDays = (value) => {
|
|
7705
7949
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -7945,8 +8189,8 @@ program.command("report").argument("[path]", "path to the project to analyze").a
|
|
|
7945
8189
|
"log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
|
|
7946
8190
|
).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
|
|
7947
8191
|
).addOption(
|
|
7948
|
-
new Option("--format <mode>", "output format: text, json, md").choices(["text", "json", "md"]).default("md")
|
|
7949
|
-
).option("--output <path>", "write rendered report to a file path").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(
|
|
8192
|
+
new Option("--format <mode>", "output format: text, json, md, html").choices(["text", "json", "md", "html"]).default("md")
|
|
8193
|
+
).option("--output <path>", "write rendered report to a file path").option("--open", "open the generated HTML report in the default browser").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(
|
|
7950
8194
|
new Option(
|
|
7951
8195
|
"--recent-window-days <days>",
|
|
7952
8196
|
"git recency window in days used for evolution volatility metrics"
|
|
@@ -7962,14 +8206,20 @@ program.command("report").argument("[path]", "path to the project to analyze").a
|
|
|
7962
8206
|
...options.output === void 0 ? {} : { outputPath: options.output },
|
|
7963
8207
|
...options.compare === void 0 ? {} : { comparePath: options.compare },
|
|
7964
8208
|
...options.snapshot === void 0 ? {} : { snapshotPath: options.snapshot },
|
|
8209
|
+
...options.open === void 0 ? {} : { open: options.open },
|
|
7965
8210
|
includeTrace: options.trace,
|
|
7966
8211
|
scoringProfile: options.scoringProfile,
|
|
7967
8212
|
recentWindowDays: options.recentWindowDays
|
|
7968
8213
|
},
|
|
7969
8214
|
logger
|
|
7970
8215
|
);
|
|
7971
|
-
if (options.output === void 0) {
|
|
8216
|
+
if (options.output === void 0 && options.format !== "html") {
|
|
7972
8217
|
process.stdout.write(`${result.rendered}
|
|
8218
|
+
`);
|
|
8219
|
+
return;
|
|
8220
|
+
}
|
|
8221
|
+
if (options.format === "html") {
|
|
8222
|
+
process.stdout.write(`${result.outputPath ?? result.rendered}
|
|
7973
8223
|
`);
|
|
7974
8224
|
}
|
|
7975
8225
|
}
|
|
@@ -7988,7 +8238,9 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
|
|
|
7988
8238
|
new Option("--format <mode>", "combined output format: text, md, json").choices(["text", "md", "json"]).default("md")
|
|
7989
8239
|
).addOption(
|
|
7990
8240
|
new Option("--detail <level>", "run detail level: compact (default), standard, full").choices(["compact", "standard", "full"]).default("compact")
|
|
7991
|
-
).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").
|
|
8241
|
+
).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").addOption(
|
|
8242
|
+
new Option("--report <format>", "write an additional report bundle during the run").choices(["html"]).default(void 0)
|
|
8243
|
+
).option("--report-output <path>", "output path for the generated report bundle").option("--open", "open the generated HTML report in the default browser").option("--no-trace", "disable trace embedding in generated snapshot").addOption(
|
|
7992
8244
|
new Option(
|
|
7993
8245
|
"--recent-window-days <days>",
|
|
7994
8246
|
"git recency window in days used for evolution volatility metrics"
|
|
@@ -8015,13 +8267,28 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
|
|
|
8015
8267
|
...options.trace === true ? { trace: explain.trace } : {}
|
|
8016
8268
|
});
|
|
8017
8269
|
if (options.snapshot !== void 0) {
|
|
8018
|
-
await
|
|
8270
|
+
await writeFile7(options.snapshot, JSON.stringify(snapshot, null, 2), "utf8");
|
|
8019
8271
|
logger.info(`snapshot written: ${options.snapshot}`);
|
|
8020
8272
|
}
|
|
8021
8273
|
const report = options.compare === void 0 ? createReport(snapshot) : createReport(
|
|
8022
8274
|
snapshot,
|
|
8023
|
-
compareSnapshots(snapshot, parseSnapshot(await
|
|
8275
|
+
compareSnapshots(snapshot, parseSnapshot(await readFile7(options.compare, "utf8")))
|
|
8024
8276
|
);
|
|
8277
|
+
if (options.report === "html") {
|
|
8278
|
+
const htmlOutputPath = await writeHtmlReportBundle(report, {
|
|
8279
|
+
repositoryPath: explain.summary.structural.targetPath,
|
|
8280
|
+
...options.reportOutput === void 0 ? {} : { outputPath: options.reportOutput }
|
|
8281
|
+
});
|
|
8282
|
+
if (options.open === true) {
|
|
8283
|
+
const opened = await openPath(`${htmlOutputPath}/index.html`);
|
|
8284
|
+
if (!opened) {
|
|
8285
|
+
logger.warn("unable to open html report automatically on this platform");
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
8288
|
+
logger.info(`html report written: ${htmlOutputPath}`);
|
|
8289
|
+
} else if (options.open === true) {
|
|
8290
|
+
logger.warn("--open has no effect unless --report html is set");
|
|
8291
|
+
}
|
|
8025
8292
|
if (options.format === "json") {
|
|
8026
8293
|
const analyzeSummaryOutput = formatAnalyzeOutput(explain.summary, "summary");
|
|
8027
8294
|
if (options.detail === "compact") {
|