@getcodesentinel/codesentinel 1.4.2 → 1.5.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 +7 -0
- package/dist/index.js +139 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ CodeSentinel combines three signals into a single, explainable risk profile:
|
|
|
11
11
|
- **Structural risk**: dependency graph topology, cycles, coupling, fan-in/fan-out, boundary violations.
|
|
12
12
|
- **Evolutionary risk**: change frequency, hotspots, bus factor, volatility.
|
|
13
13
|
- **External risk**: transitive dependency exposure, maintainer risk, staleness and abandonment indicators.
|
|
14
|
+
- Includes bounded popularity dampening (weekly npm downloads) as a secondary stability signal.
|
|
14
15
|
|
|
15
16
|
The CLI output now includes a deterministic `risk` block composed from those dimensions:
|
|
16
17
|
|
|
@@ -188,6 +189,12 @@ For `external.dependencies`, each direct dependency now exposes three signal fie
|
|
|
188
189
|
- `inheritedRiskSignals`: signals propagated from transitive dependencies in its subtree.
|
|
189
190
|
- `riskSignals`: union of `ownRiskSignals` and `inheritedRiskSignals`.
|
|
190
191
|
|
|
192
|
+
Classification lists:
|
|
193
|
+
|
|
194
|
+
- `highRiskDependencies`: **production** direct packages classified from strong **own** signals (not inherited-only signals).
|
|
195
|
+
- `highRiskDevelopmentDependencies`: same classification model for direct development dependencies.
|
|
196
|
+
- `transitiveExposureDependencies`: direct packages carrying inherited transitive exposure signals.
|
|
197
|
+
|
|
191
198
|
Propagation policy is explicit and deterministic:
|
|
192
199
|
|
|
193
200
|
- `single_maintainer`: **not propagated**
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,12 @@ var createSummaryShape = (summary) => ({
|
|
|
21
21
|
external: summary.external.available ? {
|
|
22
22
|
available: true,
|
|
23
23
|
metrics: summary.external.metrics,
|
|
24
|
-
highRiskDependenciesTop: summary.external.highRiskDependencies.slice(0, 10)
|
|
24
|
+
highRiskDependenciesTop: summary.external.highRiskDependencies.slice(0, 10),
|
|
25
|
+
highRiskDevelopmentDependenciesTop: summary.external.highRiskDevelopmentDependencies.slice(
|
|
26
|
+
0,
|
|
27
|
+
10
|
|
28
|
+
),
|
|
29
|
+
transitiveExposureDependenciesTop: summary.external.transitiveExposureDependencies.slice(0, 10)
|
|
25
30
|
} : {
|
|
26
31
|
available: false,
|
|
27
32
|
reason: summary.external.reason
|
|
@@ -640,6 +645,7 @@ var buildProjectGraphSummary = (input) => {
|
|
|
640
645
|
// ../dependency-firewall/dist/index.js
|
|
641
646
|
import { existsSync, readFileSync } from "fs";
|
|
642
647
|
import { join } from "path";
|
|
648
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
643
649
|
var round4 = (value) => Number(value.toFixed(4));
|
|
644
650
|
var normalizeNodes = (nodes) => {
|
|
645
651
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -745,7 +751,7 @@ var collectTransitiveDependencies = (rootName, nodeByName) => {
|
|
|
745
751
|
var buildExternalAnalysisSummary = (targetPath, extraction, metadataByKey, config) => {
|
|
746
752
|
const nodes = normalizeNodes(extraction.nodes);
|
|
747
753
|
const directNames = new Set(extraction.directDependencies.map((dep) => dep.name));
|
|
748
|
-
const directSpecByName = new Map(extraction.directDependencies.map((dep) => [dep.name, dep
|
|
754
|
+
const directSpecByName = new Map(extraction.directDependencies.map((dep) => [dep.name, dep]));
|
|
749
755
|
const nodeByName = new Map(nodes.map((node) => [node.name, node]));
|
|
750
756
|
const dependentsByName = /* @__PURE__ */ new Map();
|
|
751
757
|
for (const node of nodes) {
|
|
@@ -795,9 +801,11 @@ var buildExternalAnalysisSummary = (targetPath, extraction, metadataByKey, confi
|
|
|
795
801
|
allDependencies.push({
|
|
796
802
|
name: node.name,
|
|
797
803
|
direct: directNames.has(node.name),
|
|
798
|
-
|
|
804
|
+
dependencyScope: directSpecByName.get(node.name)?.scope ?? "prod",
|
|
805
|
+
requestedRange: directSpecByName.get(node.name)?.requestedRange ?? null,
|
|
799
806
|
resolvedVersion: node.version,
|
|
800
807
|
transitiveDependencies: [],
|
|
808
|
+
weeklyDownloads: metadata?.weeklyDownloads ?? null,
|
|
801
809
|
dependencyDepth,
|
|
802
810
|
fanOut: node.dependencies.length,
|
|
803
811
|
dependents,
|
|
@@ -836,7 +844,23 @@ var buildExternalAnalysisSummary = (targetPath, extraction, metadataByKey, confi
|
|
|
836
844
|
riskSignals: [...allSignals].sort((a, b) => a.localeCompare(b))
|
|
837
845
|
};
|
|
838
846
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
839
|
-
const highRiskDependencies = dependencies.filter(
|
|
847
|
+
const highRiskDependencies = dependencies.filter(
|
|
848
|
+
(dep) => dep.ownRiskSignals.includes("abandoned") || dep.ownRiskSignals.filter(
|
|
849
|
+
(signal) => signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout"
|
|
850
|
+
).length >= 2 || dep.ownRiskSignals.includes("single_maintainer") && ((dep.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold / 2 || (dep.repositoryActivity30d ?? 1) <= 0)
|
|
851
|
+
).filter((dep) => dep.dependencyScope === "prod").sort(
|
|
852
|
+
(a, b) => b.ownRiskSignals.length - a.ownRiskSignals.length || a.name.localeCompare(b.name)
|
|
853
|
+
).slice(0, config.maxHighRiskDependencies).map((dep) => dep.name);
|
|
854
|
+
const highRiskDevelopmentDependencies = dependencies.filter(
|
|
855
|
+
(dep) => dep.dependencyScope === "dev" && (dep.ownRiskSignals.includes("abandoned") || dep.ownRiskSignals.filter(
|
|
856
|
+
(signal) => signal === "high_centrality" || signal === "deep_chain" || signal === "high_fanout"
|
|
857
|
+
).length >= 2 || dep.ownRiskSignals.includes("single_maintainer") && ((dep.daysSinceLastRelease ?? 0) >= config.abandonedDaysThreshold / 2 || (dep.repositoryActivity30d ?? 1) <= 0))
|
|
858
|
+
).sort(
|
|
859
|
+
(a, b) => b.ownRiskSignals.length - a.ownRiskSignals.length || a.name.localeCompare(b.name)
|
|
860
|
+
).slice(0, config.maxHighRiskDependencies).map((dep) => dep.name);
|
|
861
|
+
const transitiveExposureDependencies = dependencies.filter((dep) => dep.inheritedRiskSignals.length > 0).sort(
|
|
862
|
+
(a, b) => b.inheritedRiskSignals.length - a.inheritedRiskSignals.length || a.name.localeCompare(b.name)
|
|
863
|
+
).map((dep) => dep.name);
|
|
840
864
|
const singleMaintainerDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("single_maintainer")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
841
865
|
const abandonedDependencies = dependencies.filter((dep) => dep.ownRiskSignals.includes("abandoned")).map((dep) => dep.name).sort((a, b) => a.localeCompare(b));
|
|
842
866
|
return {
|
|
@@ -845,6 +869,8 @@ var buildExternalAnalysisSummary = (targetPath, extraction, metadataByKey, confi
|
|
|
845
869
|
metrics: {
|
|
846
870
|
totalDependencies: allDependencies.length,
|
|
847
871
|
directDependencies: dependencies.length,
|
|
872
|
+
directProductionDependencies: dependencies.filter((dependency) => dependency.dependencyScope === "prod").length,
|
|
873
|
+
directDevelopmentDependencies: dependencies.filter((dependency) => dependency.dependencyScope === "dev").length,
|
|
848
874
|
transitiveDependencies: allDependencies.length - dependencies.length,
|
|
849
875
|
dependencyDepth: maxDepth,
|
|
850
876
|
lockfileKind: extraction.kind,
|
|
@@ -852,6 +878,8 @@ var buildExternalAnalysisSummary = (targetPath, extraction, metadataByKey, confi
|
|
|
852
878
|
},
|
|
853
879
|
dependencies,
|
|
854
880
|
highRiskDependencies,
|
|
881
|
+
highRiskDevelopmentDependencies,
|
|
882
|
+
transitiveExposureDependencies,
|
|
855
883
|
singleMaintainerDependencies,
|
|
856
884
|
abandonedDependencies,
|
|
857
885
|
centralityRanking
|
|
@@ -900,20 +928,23 @@ var selectLockfile = (repositoryPath) => {
|
|
|
900
928
|
var parsePackageJson = (raw) => {
|
|
901
929
|
const parsed = JSON.parse(raw);
|
|
902
930
|
const merged = /* @__PURE__ */ new Map();
|
|
903
|
-
|
|
904
|
-
parsed.dependencies,
|
|
905
|
-
parsed.devDependencies,
|
|
906
|
-
parsed.optionalDependencies,
|
|
907
|
-
parsed.peerDependencies
|
|
908
|
-
]) {
|
|
931
|
+
const addBlock = (block, scope) => {
|
|
909
932
|
if (block === void 0) {
|
|
910
|
-
|
|
933
|
+
return;
|
|
911
934
|
}
|
|
912
935
|
for (const [name, versionRange] of Object.entries(block)) {
|
|
913
|
-
merged.
|
|
936
|
+
const existing = merged.get(name);
|
|
937
|
+
if (existing?.scope === "prod" && scope === "dev") {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
merged.set(name, { name, requestedRange: versionRange, scope });
|
|
914
941
|
}
|
|
915
|
-
}
|
|
916
|
-
|
|
942
|
+
};
|
|
943
|
+
addBlock(parsed.dependencies, "prod");
|
|
944
|
+
addBlock(parsed.optionalDependencies, "prod");
|
|
945
|
+
addBlock(parsed.peerDependencies, "prod");
|
|
946
|
+
addBlock(parsed.devDependencies, "dev");
|
|
947
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
917
948
|
};
|
|
918
949
|
var parsePackageLock = (raw, directSpecs) => {
|
|
919
950
|
const parsed = JSON.parse(raw);
|
|
@@ -1219,6 +1250,7 @@ var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
|
|
|
1219
1250
|
const directSpecs = parsePackageJson(packageJson.raw);
|
|
1220
1251
|
const extraction = parseExtraction(lockfile.kind, lockfile.raw, directSpecs);
|
|
1221
1252
|
const config = withDefaults(input.config);
|
|
1253
|
+
const directNames = new Set(extraction.directDependencies.map((dependency) => dependency.name));
|
|
1222
1254
|
onProgress?.({
|
|
1223
1255
|
stage: "lockfile_parsed",
|
|
1224
1256
|
dependencyNodes: extraction.nodes.length,
|
|
@@ -1232,7 +1264,9 @@ var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
|
|
|
1232
1264
|
async (node) => {
|
|
1233
1265
|
const result = {
|
|
1234
1266
|
key: `${node.name}@${node.version}`,
|
|
1235
|
-
metadata: await metadataProvider.getMetadata(node.name, node.version
|
|
1267
|
+
metadata: await metadataProvider.getMetadata(node.name, node.version, {
|
|
1268
|
+
directDependency: directNames.has(node.name)
|
|
1269
|
+
})
|
|
1236
1270
|
};
|
|
1237
1271
|
completed += 1;
|
|
1238
1272
|
onProgress?.({
|
|
@@ -1274,7 +1308,35 @@ var analyzeDependencyExposure = async (input, metadataProvider, onProgress) => {
|
|
|
1274
1308
|
};
|
|
1275
1309
|
}
|
|
1276
1310
|
};
|
|
1311
|
+
var parseRetryAfterMs = (value) => {
|
|
1312
|
+
if (value === null) {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
const seconds = Number.parseInt(value, 10);
|
|
1316
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
return seconds * 1e3;
|
|
1320
|
+
};
|
|
1321
|
+
var shouldRetryStatus = (status) => status === 429 || status >= 500;
|
|
1322
|
+
var fetchJsonWithRetry = async (url, options) => {
|
|
1323
|
+
for (let attempt = 0; attempt <= options.retries; attempt += 1) {
|
|
1324
|
+
const response = await fetch(url);
|
|
1325
|
+
if (response.ok) {
|
|
1326
|
+
return await response.json();
|
|
1327
|
+
}
|
|
1328
|
+
if (!shouldRetryStatus(response.status) || attempt === options.retries) {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
1332
|
+
const backoffMs = retryAfterMs ?? options.baseDelayMs * 2 ** attempt;
|
|
1333
|
+
await sleep(backoffMs);
|
|
1334
|
+
}
|
|
1335
|
+
return null;
|
|
1336
|
+
};
|
|
1277
1337
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1338
|
+
var MAX_RETRIES = 3;
|
|
1339
|
+
var RETRY_BASE_DELAY_MS = 500;
|
|
1278
1340
|
var round42 = (value) => Number(value.toFixed(4));
|
|
1279
1341
|
var parseDate = (iso) => {
|
|
1280
1342
|
if (iso === void 0) {
|
|
@@ -1285,19 +1347,36 @@ var parseDate = (iso) => {
|
|
|
1285
1347
|
};
|
|
1286
1348
|
var NpmRegistryMetadataProvider = class {
|
|
1287
1349
|
cache = /* @__PURE__ */ new Map();
|
|
1288
|
-
async
|
|
1350
|
+
async fetchWeeklyDownloads(name) {
|
|
1351
|
+
const encodedName = encodeURIComponent(name);
|
|
1352
|
+
const payload = await fetchJsonWithRetry(
|
|
1353
|
+
`https://api.npmjs.org/downloads/point/last-week/${encodedName}`,
|
|
1354
|
+
{ retries: MAX_RETRIES, baseDelayMs: RETRY_BASE_DELAY_MS }
|
|
1355
|
+
);
|
|
1356
|
+
if (payload === null) {
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
const downloads = payload.downloads;
|
|
1360
|
+
if (typeof downloads !== "number" || Number.isNaN(downloads) || downloads < 0) {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
return Math.floor(downloads);
|
|
1364
|
+
}
|
|
1365
|
+
async getMetadata(name, version2, context) {
|
|
1289
1366
|
const key = `${name}@${version2}`;
|
|
1290
1367
|
if (this.cache.has(key)) {
|
|
1291
1368
|
return this.cache.get(key) ?? null;
|
|
1292
1369
|
}
|
|
1293
1370
|
try {
|
|
1294
1371
|
const encodedName = encodeURIComponent(name);
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1372
|
+
const payload = await fetchJsonWithRetry(
|
|
1373
|
+
`https://registry.npmjs.org/${encodedName}`,
|
|
1374
|
+
{ retries: MAX_RETRIES, baseDelayMs: RETRY_BASE_DELAY_MS }
|
|
1375
|
+
);
|
|
1376
|
+
if (payload === null) {
|
|
1297
1377
|
this.cache.set(key, null);
|
|
1298
1378
|
return null;
|
|
1299
1379
|
}
|
|
1300
|
-
const payload = await response.json();
|
|
1301
1380
|
const timeEntries = payload.time ?? {};
|
|
1302
1381
|
const publishDates = Object.entries(timeEntries).filter(([tag]) => tag !== "created" && tag !== "modified").map(([, date]) => parseDate(date)).filter((value) => value !== null).sort((a, b) => a - b);
|
|
1303
1382
|
const modifiedAt = parseDate(timeEntries["modified"]);
|
|
@@ -1318,9 +1397,11 @@ var NpmRegistryMetadataProvider = class {
|
|
|
1318
1397
|
}
|
|
1319
1398
|
const maintainers = payload.maintainers ?? [];
|
|
1320
1399
|
const maintainerCount = maintainers.length > 0 ? maintainers.length : null;
|
|
1400
|
+
const weeklyDownloads = context.directDependency ? await this.fetchWeeklyDownloads(name).catch(() => null) : null;
|
|
1321
1401
|
const metadata = {
|
|
1322
1402
|
name,
|
|
1323
1403
|
version: version2,
|
|
1404
|
+
weeklyDownloads,
|
|
1324
1405
|
maintainerCount,
|
|
1325
1406
|
releaseFrequencyDays,
|
|
1326
1407
|
daysSinceLastRelease,
|
|
@@ -1336,7 +1417,7 @@ var NpmRegistryMetadataProvider = class {
|
|
|
1336
1417
|
}
|
|
1337
1418
|
};
|
|
1338
1419
|
var NoopMetadataProvider = class {
|
|
1339
|
-
async getMetadata(_name, _version) {
|
|
1420
|
+
async getMetadata(_name, _version, _context) {
|
|
1340
1421
|
return null;
|
|
1341
1422
|
}
|
|
1342
1423
|
};
|
|
@@ -1899,25 +1980,18 @@ var DEFAULT_RISK_ENGINE_CONFIG = {
|
|
|
1899
1980
|
inheritedSignalMultiplier: 0.45,
|
|
1900
1981
|
// At this age, staleness reaches 50% risk.
|
|
1901
1982
|
abandonedHalfLifeDays: 540,
|
|
1902
|
-
missingMetadataPenalty: 0.5
|
|
1983
|
+
missingMetadataPenalty: 0.5,
|
|
1984
|
+
// At this download volume, popularity reaches 50% of its dampening effect.
|
|
1985
|
+
popularityHalfLifeDownloads: 1e5,
|
|
1986
|
+
// Popularity can only reduce dependency risk by this fraction.
|
|
1987
|
+
popularityMaxDampening: 0.12
|
|
1903
1988
|
},
|
|
1904
1989
|
externalDimension: {
|
|
1905
1990
|
topDependencyPercentile: 0.85,
|
|
1906
1991
|
dependencyDepthHalfLife: 6
|
|
1907
1992
|
}
|
|
1908
1993
|
};
|
|
1909
|
-
var
|
|
1910
|
-
if (Number.isNaN(value)) {
|
|
1911
|
-
return 0;
|
|
1912
|
-
}
|
|
1913
|
-
if (value <= 0) {
|
|
1914
|
-
return 0;
|
|
1915
|
-
}
|
|
1916
|
-
if (value >= 1) {
|
|
1917
|
-
return 1;
|
|
1918
|
-
}
|
|
1919
|
-
return value;
|
|
1920
|
-
};
|
|
1994
|
+
var toUnitInterval = (value) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
1921
1995
|
var round44 = (value) => Number(value.toFixed(4));
|
|
1922
1996
|
var average = (values) => {
|
|
1923
1997
|
if (values.length === 0) {
|
|
@@ -1934,7 +2008,7 @@ var percentile = (values, p) => {
|
|
|
1934
2008
|
return values[0] ?? 0;
|
|
1935
2009
|
}
|
|
1936
2010
|
const sorted = [...values].sort((a, b) => a - b);
|
|
1937
|
-
const position =
|
|
2011
|
+
const position = toUnitInterval(p) * (sorted.length - 1);
|
|
1938
2012
|
const lowerIndex = Math.floor(position);
|
|
1939
2013
|
const upperIndex = Math.ceil(position);
|
|
1940
2014
|
const lower = sorted[lowerIndex] ?? 0;
|
|
@@ -1946,18 +2020,18 @@ var percentile = (values, p) => {
|
|
|
1946
2020
|
return lower + (upper - lower) * ratio;
|
|
1947
2021
|
};
|
|
1948
2022
|
var saturatingComposite = (baseline, amplifications) => {
|
|
1949
|
-
let value =
|
|
2023
|
+
let value = toUnitInterval(baseline);
|
|
1950
2024
|
for (const amplification of amplifications) {
|
|
1951
|
-
const boundedAmplification =
|
|
2025
|
+
const boundedAmplification = toUnitInterval(amplification);
|
|
1952
2026
|
value += (1 - value) * boundedAmplification;
|
|
1953
2027
|
}
|
|
1954
|
-
return
|
|
2028
|
+
return toUnitInterval(value);
|
|
1955
2029
|
};
|
|
1956
2030
|
var halfLifeRisk = (value, halfLife) => {
|
|
1957
2031
|
if (value <= 0 || halfLife <= 0) {
|
|
1958
2032
|
return 0;
|
|
1959
2033
|
}
|
|
1960
|
-
return
|
|
2034
|
+
return toUnitInterval(value / (value + halfLife));
|
|
1961
2035
|
};
|
|
1962
2036
|
var normalizeWeights = (weights, enabled) => {
|
|
1963
2037
|
let total = 0;
|
|
@@ -2004,7 +2078,7 @@ var normalizeWithScale = (value, scale) => {
|
|
|
2004
2078
|
if (scale.upper <= scale.lower) {
|
|
2005
2079
|
return value > 0 ? 1 : 0;
|
|
2006
2080
|
}
|
|
2007
|
-
return
|
|
2081
|
+
return toUnitInterval((value - scale.lower) / (scale.upper - scale.lower));
|
|
2008
2082
|
};
|
|
2009
2083
|
var normalizePath2 = (path) => path.replaceAll("\\", "/");
|
|
2010
2084
|
var dependencySignalWeights = {
|
|
@@ -2030,7 +2104,7 @@ var computeDependencySignalScore = (ownSignals, inheritedSignals, inheritedSigna
|
|
|
2030
2104
|
if (maxWeightedTotal <= 0) {
|
|
2031
2105
|
return 0;
|
|
2032
2106
|
}
|
|
2033
|
-
return
|
|
2107
|
+
return toUnitInterval(weightedTotal / maxWeightedTotal);
|
|
2034
2108
|
};
|
|
2035
2109
|
var computeDependencyScores = (external, config) => {
|
|
2036
2110
|
if (!external.available) {
|
|
@@ -2065,7 +2139,7 @@ var computeDependencyScores = (external, config) => {
|
|
|
2065
2139
|
dependency.inheritedRiskSignals,
|
|
2066
2140
|
config.dependencySignals.inheritedSignalMultiplier
|
|
2067
2141
|
);
|
|
2068
|
-
const maintainerConcentrationRisk = dependency.maintainerCount === null ? config.dependencySignals.missingMetadataPenalty :
|
|
2142
|
+
const maintainerConcentrationRisk = dependency.maintainerCount === null ? config.dependencySignals.missingMetadataPenalty : toUnitInterval(1 / Math.max(1, dependency.maintainerCount));
|
|
2069
2143
|
const stalenessRisk = dependency.daysSinceLastRelease === null ? config.dependencySignals.missingMetadataPenalty : halfLifeRisk(
|
|
2070
2144
|
dependency.daysSinceLastRelease,
|
|
2071
2145
|
config.dependencySignals.abandonedHalfLifeDays
|
|
@@ -2076,11 +2150,17 @@ var computeDependencyScores = (external, config) => {
|
|
|
2076
2150
|
);
|
|
2077
2151
|
const centralityRisk = normalizeWithScale(logScale(dependency.dependents), dependentScale);
|
|
2078
2152
|
const chainDepthRisk = normalizeWithScale(dependency.dependencyDepth, chainDepthScale);
|
|
2079
|
-
const busFactorRisk = dependency.busFactor === null ? config.dependencySignals.missingMetadataPenalty :
|
|
2153
|
+
const busFactorRisk = dependency.busFactor === null ? config.dependencySignals.missingMetadataPenalty : toUnitInterval(1 / Math.max(1, dependency.busFactor));
|
|
2080
2154
|
const weights = config.dependencyFactorWeights;
|
|
2081
|
-
const
|
|
2155
|
+
const baseScore = toUnitInterval(
|
|
2082
2156
|
signalScore * weights.signals + stalenessRisk * weights.staleness + maintainerConcentrationRisk * weights.maintainerConcentration + transitiveBurdenRisk * weights.transitiveBurden + centralityRisk * weights.centrality + chainDepthRisk * weights.chainDepth + busFactorRisk * weights.busFactorRisk
|
|
2083
2157
|
);
|
|
2158
|
+
const hasHardRiskSignal = dependency.ownRiskSignals.includes("abandoned") || dependency.ownRiskSignals.includes("metadata_unavailable") || dependency.ownRiskSignals.includes("single_maintainer");
|
|
2159
|
+
const popularityDampener = dependency.weeklyDownloads === null || hasHardRiskSignal ? 1 : 1 - halfLifeRisk(
|
|
2160
|
+
dependency.weeklyDownloads,
|
|
2161
|
+
config.dependencySignals.popularityHalfLifeDownloads
|
|
2162
|
+
) * config.dependencySignals.popularityMaxDampening;
|
|
2163
|
+
const normalizedScore = toUnitInterval(baseScore * popularityDampener);
|
|
2084
2164
|
return {
|
|
2085
2165
|
dependency: dependency.name,
|
|
2086
2166
|
score: round44(normalizedScore * 100),
|
|
@@ -2098,7 +2178,7 @@ var computeDependencyScores = (external, config) => {
|
|
|
2098
2178
|
external.metrics.dependencyDepth,
|
|
2099
2179
|
config.externalDimension.dependencyDepthHalfLife
|
|
2100
2180
|
);
|
|
2101
|
-
const repositoryExternalPressure =
|
|
2181
|
+
const repositoryExternalPressure = toUnitInterval(
|
|
2102
2182
|
highDependencyRisk * 0.5 + averageDependencyRisk * 0.3 + depthRisk * 0.2
|
|
2103
2183
|
);
|
|
2104
2184
|
return {
|
|
@@ -2166,8 +2246,8 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
|
|
|
2166
2246
|
const averageRisk = average(
|
|
2167
2247
|
files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
|
|
2168
2248
|
);
|
|
2169
|
-
const cycleSizeRisk =
|
|
2170
|
-
const score = round44(
|
|
2249
|
+
const cycleSizeRisk = toUnitInterval((files.length - 1) / 5);
|
|
2250
|
+
const score = round44(toUnitInterval(averageRisk * 0.75 + cycleSizeRisk * 0.25) * 100);
|
|
2171
2251
|
cycleClusterCount += 1;
|
|
2172
2252
|
clusters.push({
|
|
2173
2253
|
id: `cycle:${cycleClusterCount}`,
|
|
@@ -2241,7 +2321,7 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
|
|
|
2241
2321
|
files.map((filePath) => fileScoresByFile.get(filePath)?.normalizedScore ?? 0)
|
|
2242
2322
|
);
|
|
2243
2323
|
const meanCoupling = average(componentPairs.map((pair) => pair.couplingScore));
|
|
2244
|
-
const score = round44(
|
|
2324
|
+
const score = round44(toUnitInterval(meanFileRisk * 0.65 + meanCoupling * 0.35) * 100);
|
|
2245
2325
|
couplingClusterCount += 1;
|
|
2246
2326
|
clusters.push({
|
|
2247
2327
|
id: `coupling:${couplingClusterCount}`,
|
|
@@ -2289,10 +2369,10 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2289
2369
|
const fanOutRisk = normalizeWithScale(logScale(file.fanOut), fanOutScale);
|
|
2290
2370
|
const depthRisk = normalizeWithScale(file.depth, depthScale);
|
|
2291
2371
|
const structuralWeights = config.structuralFactorWeights;
|
|
2292
|
-
const structuralFactor =
|
|
2372
|
+
const structuralFactor = toUnitInterval(
|
|
2293
2373
|
fanInRisk * structuralWeights.fanIn + fanOutRisk * structuralWeights.fanOut + depthRisk * structuralWeights.depth + inCycle * structuralWeights.cycleParticipation
|
|
2294
2374
|
);
|
|
2295
|
-
const structuralCentrality =
|
|
2375
|
+
const structuralCentrality = toUnitInterval((fanInRisk + fanOutRisk) / 2);
|
|
2296
2376
|
let evolutionFactor = 0;
|
|
2297
2377
|
const evolutionMetrics = evolutionByFile.get(filePath);
|
|
2298
2378
|
if (evolution.available && evolutionMetrics !== void 0) {
|
|
@@ -2304,16 +2384,16 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2304
2384
|
logScale(evolutionMetrics.churnTotal),
|
|
2305
2385
|
evolutionScales.churnTotal
|
|
2306
2386
|
);
|
|
2307
|
-
const volatilityRisk =
|
|
2308
|
-
const ownershipConcentrationRisk =
|
|
2309
|
-
const busFactorRisk =
|
|
2387
|
+
const volatilityRisk = toUnitInterval(evolutionMetrics.recentVolatility);
|
|
2388
|
+
const ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShare);
|
|
2389
|
+
const busFactorRisk = toUnitInterval(1 - normalizeWithScale(evolutionMetrics.busFactor, evolutionScales.busFactor));
|
|
2310
2390
|
const evolutionWeights = config.evolutionFactorWeights;
|
|
2311
|
-
evolutionFactor =
|
|
2391
|
+
evolutionFactor = toUnitInterval(
|
|
2312
2392
|
frequencyRisk * evolutionWeights.frequency + churnRisk * evolutionWeights.churn + volatilityRisk * evolutionWeights.recentVolatility + ownershipConcentrationRisk * evolutionWeights.ownershipConcentration + busFactorRisk * evolutionWeights.busFactorRisk
|
|
2313
2393
|
);
|
|
2314
2394
|
}
|
|
2315
|
-
const dependencyAffinity =
|
|
2316
|
-
const externalFactor = external.available ?
|
|
2395
|
+
const dependencyAffinity = toUnitInterval(structuralCentrality * 0.6 + evolutionFactor * 0.4);
|
|
2396
|
+
const externalFactor = external.available ? toUnitInterval(dependencyComputation.repositoryExternalPressure * dependencyAffinity) : 0;
|
|
2317
2397
|
const baseline = structuralFactor * dimensionWeights.structural + evolutionFactor * dimensionWeights.evolution + externalFactor * dimensionWeights.external;
|
|
2318
2398
|
const interactions = [
|
|
2319
2399
|
structuralFactor * evolutionFactor * config.interactionWeights.structuralEvolution,
|
|
@@ -2359,7 +2439,7 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2359
2439
|
const moduleScores = [...moduleFiles.entries()].map(([module, values]) => {
|
|
2360
2440
|
const averageScore = average(values);
|
|
2361
2441
|
const peakScore = values.reduce((max, value) => Math.max(max, value), 0);
|
|
2362
|
-
const normalizedScore =
|
|
2442
|
+
const normalizedScore = toUnitInterval(averageScore * 0.65 + peakScore * 0.35);
|
|
2363
2443
|
return {
|
|
2364
2444
|
module,
|
|
2365
2445
|
score: round44(normalizedScore * 100),
|
|
@@ -2374,10 +2454,10 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2374
2454
|
percentile(externalPressures, config.amplificationZone.percentileThreshold)
|
|
2375
2455
|
);
|
|
2376
2456
|
const dependencyAmplificationZones = fileScores.map((fileScore) => {
|
|
2377
|
-
const intensity =
|
|
2457
|
+
const intensity = toUnitInterval(
|
|
2378
2458
|
fileScore.factors.external * Math.max(fileScore.factors.structural, fileScore.factors.evolution)
|
|
2379
2459
|
);
|
|
2380
|
-
const normalizedZoneScore =
|
|
2460
|
+
const normalizedZoneScore = toUnitInterval(intensity * 0.7 + fileScore.normalizedScore * 0.3);
|
|
2381
2461
|
return {
|
|
2382
2462
|
file: fileScore.file,
|
|
2383
2463
|
score: round44(normalizedZoneScore * 100),
|
|
@@ -2398,7 +2478,7 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2398
2478
|
);
|
|
2399
2479
|
const dependencyAmplification = average(
|
|
2400
2480
|
dependencyAmplificationZones.map(
|
|
2401
|
-
(zone) =>
|
|
2481
|
+
(zone) => toUnitInterval(zone.externalPressure * zone.score / 100)
|
|
2402
2482
|
)
|
|
2403
2483
|
);
|
|
2404
2484
|
const repositoryBaseline = structuralDimension * dimensionWeights.structural + evolutionDimension * dimensionWeights.evolution + externalDimension * dimensionWeights.external;
|