@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 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.requestedRange]));
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
- requestedRange: directSpecByName.get(node.name) ?? null,
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((dep) => dep.riskSignals.length > 1).sort((a, b) => b.riskSignals.length - a.riskSignals.length || a.name.localeCompare(b.name)).slice(0, config.maxHighRiskDependencies).map((dep) => dep.name);
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
- for (const block of [
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
- continue;
933
+ return;
911
934
  }
912
935
  for (const [name, versionRange] of Object.entries(block)) {
913
- merged.set(name, versionRange);
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
- return [...merged.entries()].map(([name, requestedRange]) => ({ name, requestedRange })).sort((a, b) => a.name.localeCompare(b.name));
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 getMetadata(name, version2) {
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 response = await fetch(`https://registry.npmjs.org/${encodedName}`);
1296
- if (!response.ok) {
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 clamp01 = (value) => {
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 = clamp01(p) * (sorted.length - 1);
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 = clamp01(baseline);
2023
+ let value = toUnitInterval(baseline);
1950
2024
  for (const amplification of amplifications) {
1951
- const boundedAmplification = clamp01(amplification);
2025
+ const boundedAmplification = toUnitInterval(amplification);
1952
2026
  value += (1 - value) * boundedAmplification;
1953
2027
  }
1954
- return clamp01(value);
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 clamp01(value / (value + halfLife));
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 clamp01((value - scale.lower) / (scale.upper - scale.lower));
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 clamp01(weightedTotal / maxWeightedTotal);
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 : clamp01(1 / Math.max(1, dependency.maintainerCount));
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 : clamp01(1 / Math.max(1, dependency.busFactor));
2153
+ const busFactorRisk = dependency.busFactor === null ? config.dependencySignals.missingMetadataPenalty : toUnitInterval(1 / Math.max(1, dependency.busFactor));
2080
2154
  const weights = config.dependencyFactorWeights;
2081
- const normalizedScore = clamp01(
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 = clamp01(
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 = clamp01((files.length - 1) / 5);
2170
- const score = round44(clamp01(averageRisk * 0.75 + cycleSizeRisk * 0.25) * 100);
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(clamp01(meanFileRisk * 0.65 + meanCoupling * 0.35) * 100);
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 = clamp01(
2372
+ const structuralFactor = toUnitInterval(
2293
2373
  fanInRisk * structuralWeights.fanIn + fanOutRisk * structuralWeights.fanOut + depthRisk * structuralWeights.depth + inCycle * structuralWeights.cycleParticipation
2294
2374
  );
2295
- const structuralCentrality = clamp01((fanInRisk + fanOutRisk) / 2);
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 = clamp01(evolutionMetrics.recentVolatility);
2308
- const ownershipConcentrationRisk = clamp01(evolutionMetrics.topAuthorShare);
2309
- const busFactorRisk = clamp01(1 - normalizeWithScale(evolutionMetrics.busFactor, evolutionScales.busFactor));
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 = clamp01(
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 = clamp01(structuralCentrality * 0.6 + evolutionFactor * 0.4);
2316
- const externalFactor = external.available ? clamp01(dependencyComputation.repositoryExternalPressure * dependencyAffinity) : 0;
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 = clamp01(averageScore * 0.65 + peakScore * 0.35);
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 = clamp01(
2457
+ const intensity = toUnitInterval(
2378
2458
  fileScore.factors.external * Math.max(fileScore.factors.structural, fileScore.factors.evolution)
2379
2459
  );
2380
- const normalizedZoneScore = clamp01(intensity * 0.7 + fileScore.normalizedScore * 0.3);
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) => clamp01(zone.externalPressure * zone.score / 100)
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;