@getcodesentinel/codesentinel 1.6.5 → 1.7.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 +53 -0
- package/dist/index.js +827 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1392,6 +1392,223 @@ var createSummaryShape = (summary) => ({
|
|
|
1392
1392
|
});
|
|
1393
1393
|
var formatAnalyzeOutput = (summary, mode) => mode === "json" ? JSON.stringify(summary, null, 2) : JSON.stringify(createSummaryShape(summary), null, 2);
|
|
1394
1394
|
|
|
1395
|
+
// src/application/format-explain-output.ts
|
|
1396
|
+
var sortFactorByContribution = (left, right) => right.contribution - left.contribution || left.factorId.localeCompare(right.factorId);
|
|
1397
|
+
var toRiskBand = (score) => {
|
|
1398
|
+
if (score < 25) {
|
|
1399
|
+
return "low";
|
|
1400
|
+
}
|
|
1401
|
+
if (score < 50) {
|
|
1402
|
+
return "moderate";
|
|
1403
|
+
}
|
|
1404
|
+
if (score < 75) {
|
|
1405
|
+
return "high";
|
|
1406
|
+
}
|
|
1407
|
+
return "very_high";
|
|
1408
|
+
};
|
|
1409
|
+
var factorLabelById = {
|
|
1410
|
+
"repository.structural": "Structural complexity",
|
|
1411
|
+
"repository.evolution": "Change volatility",
|
|
1412
|
+
"repository.external": "External dependency pressure",
|
|
1413
|
+
"repository.composite.interactions": "Intersection amplification",
|
|
1414
|
+
"file.structural": "File structural complexity",
|
|
1415
|
+
"file.evolution": "File change volatility",
|
|
1416
|
+
"file.external": "File external pressure",
|
|
1417
|
+
"file.composite.interactions": "File interaction amplification",
|
|
1418
|
+
"module.average_file_risk": "Average file risk",
|
|
1419
|
+
"module.peak_file_risk": "Peak file risk",
|
|
1420
|
+
"dependency.signals": "Dependency risk signals",
|
|
1421
|
+
"dependency.staleness": "Dependency staleness",
|
|
1422
|
+
"dependency.maintainer_concentration": "Maintainer concentration",
|
|
1423
|
+
"dependency.topology": "Dependency topology pressure",
|
|
1424
|
+
"dependency.bus_factor": "Dependency bus factor",
|
|
1425
|
+
"dependency.popularity_dampening": "Popularity dampening"
|
|
1426
|
+
};
|
|
1427
|
+
var formatFactorLabel = (factorId) => factorLabelById[factorId] ?? factorId;
|
|
1428
|
+
var formatNumber = (value) => value === null || value === void 0 ? "n/a" : `${value}`;
|
|
1429
|
+
var formatFactorSummary = (factor) => `${formatFactorLabel(factor.factorId)} (+${factor.contribution}, confidence=${factor.confidence})`;
|
|
1430
|
+
var formatFactorEvidence = (factor) => {
|
|
1431
|
+
if (factor.factorId === "repository.structural") {
|
|
1432
|
+
return `structural dimension=${formatNumber(factor.rawMetrics["structuralDimension"])}`;
|
|
1433
|
+
}
|
|
1434
|
+
if (factor.factorId === "repository.evolution") {
|
|
1435
|
+
return `evolution dimension=${formatNumber(factor.rawMetrics["evolutionDimension"])}`;
|
|
1436
|
+
}
|
|
1437
|
+
if (factor.factorId === "repository.external") {
|
|
1438
|
+
return `external dimension=${formatNumber(factor.rawMetrics["externalDimension"])}`;
|
|
1439
|
+
}
|
|
1440
|
+
if (factor.factorId === "repository.composite.interactions") {
|
|
1441
|
+
return `structural\xD7evolution=${formatNumber(factor.rawMetrics["structuralEvolution"])}, central instability=${formatNumber(factor.rawMetrics["centralInstability"])}, dependency amplification=${formatNumber(factor.rawMetrics["dependencyAmplification"])}`;
|
|
1442
|
+
}
|
|
1443
|
+
if (factor.factorId === "file.structural") {
|
|
1444
|
+
return `fanIn=${formatNumber(factor.rawMetrics["fanIn"])}, fanOut=${formatNumber(factor.rawMetrics["fanOut"])}, depth=${formatNumber(factor.rawMetrics["depth"])}, inCycle=${formatNumber(factor.rawMetrics["cycleParticipation"])}`;
|
|
1445
|
+
}
|
|
1446
|
+
if (factor.factorId === "file.evolution") {
|
|
1447
|
+
return `commitCount=${formatNumber(factor.rawMetrics["commitCount"])}, churnTotal=${formatNumber(factor.rawMetrics["churnTotal"])}, recentVolatility=${formatNumber(factor.rawMetrics["recentVolatility"])}`;
|
|
1448
|
+
}
|
|
1449
|
+
if (factor.factorId === "file.external") {
|
|
1450
|
+
return `repositoryExternalPressure=${formatNumber(factor.rawMetrics["repositoryExternalPressure"])}, dependencyAffinity=${formatNumber(factor.rawMetrics["dependencyAffinity"])}`;
|
|
1451
|
+
}
|
|
1452
|
+
if (factor.factorId === "file.composite.interactions") {
|
|
1453
|
+
return `structural\xD7evolution=${formatNumber(factor.rawMetrics["structuralEvolutionInteraction"])}, central instability=${formatNumber(factor.rawMetrics["centralInstabilityInteraction"])}, dependency amplification=${formatNumber(factor.rawMetrics["dependencyAmplificationInteraction"])}`;
|
|
1454
|
+
}
|
|
1455
|
+
return "evidence available in trace";
|
|
1456
|
+
};
|
|
1457
|
+
var findRepositoryTarget = (targets) => targets.find((target) => target.targetType === "repository");
|
|
1458
|
+
var buildRepositoryActions = (payload, repositoryTarget) => {
|
|
1459
|
+
if (repositoryTarget === void 0) {
|
|
1460
|
+
return ["No repository trace available."];
|
|
1461
|
+
}
|
|
1462
|
+
const topHotspots = payload.summary.risk.hotspots.slice(0, 3).map((hotspot) => hotspot.file);
|
|
1463
|
+
const highRiskDependencies = payload.summary.external.available ? payload.summary.external.highRiskDependencies.slice(0, 3) : [];
|
|
1464
|
+
const actions = [];
|
|
1465
|
+
for (const lever of repositoryTarget.reductionLevers) {
|
|
1466
|
+
if (lever.factorId === "repository.evolution") {
|
|
1467
|
+
actions.push(
|
|
1468
|
+
`Reduce volatility/churn in top hotspots first: ${topHotspots.join(", ") || "no hotspots available"}.`
|
|
1469
|
+
);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (lever.factorId === "repository.structural") {
|
|
1473
|
+
actions.push(
|
|
1474
|
+
`Lower fan-in/fan-out and break cycles in central files: ${topHotspots.join(", ") || "no hotspots available"}.`
|
|
1475
|
+
);
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
if (lever.factorId === "repository.composite.interactions") {
|
|
1479
|
+
actions.push(
|
|
1480
|
+
"Stabilize central files before refactors; interaction effects are amplifying risk."
|
|
1481
|
+
);
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
if (lever.factorId === "repository.external") {
|
|
1485
|
+
actions.push(
|
|
1486
|
+
`Review high-risk direct dependencies: ${highRiskDependencies.join(", ") || "none detected"}.`
|
|
1487
|
+
);
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (actions.length === 0) {
|
|
1492
|
+
actions.push("No clear reduction levers available from current trace.");
|
|
1493
|
+
}
|
|
1494
|
+
return actions.slice(0, 3);
|
|
1495
|
+
};
|
|
1496
|
+
var renderTargetText = (target) => {
|
|
1497
|
+
const lines = [];
|
|
1498
|
+
lines.push(`${target.targetType}: ${target.targetId}`);
|
|
1499
|
+
lines.push(` score: ${target.totalScore} (${target.normalizedScore})`);
|
|
1500
|
+
lines.push(" top factors:");
|
|
1501
|
+
const topFactors = [...target.factors].sort(sortFactorByContribution).slice(0, 5);
|
|
1502
|
+
for (const factor of topFactors) {
|
|
1503
|
+
lines.push(
|
|
1504
|
+
` - ${formatFactorSummary(factor)}`
|
|
1505
|
+
);
|
|
1506
|
+
lines.push(
|
|
1507
|
+
` evidence: ${formatFactorEvidence(factor)}`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
lines.push(" reduction levers:");
|
|
1511
|
+
for (const lever of target.reductionLevers) {
|
|
1512
|
+
lines.push(
|
|
1513
|
+
` - ${formatFactorLabel(lever.factorId)} | estimatedImpact=${lever.estimatedImpact}`
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
return lines.join("\n");
|
|
1517
|
+
};
|
|
1518
|
+
var renderText = (payload) => {
|
|
1519
|
+
const lines = [];
|
|
1520
|
+
const repositoryTarget = findRepositoryTarget(payload.selectedTargets) ?? findRepositoryTarget(payload.trace.targets);
|
|
1521
|
+
const repositoryTopFactors = repositoryTarget === void 0 ? [] : [...repositoryTarget.factors].sort(sortFactorByContribution).slice(0, 3);
|
|
1522
|
+
const compositeFactors = repositoryTopFactors.filter((factor) => factor.family === "composite");
|
|
1523
|
+
lines.push(`target: ${payload.summary.structural.targetPath}`);
|
|
1524
|
+
lines.push(`repositoryScore: ${payload.summary.risk.repositoryScore}`);
|
|
1525
|
+
lines.push(`riskBand: ${toRiskBand(payload.summary.risk.repositoryScore)}`);
|
|
1526
|
+
lines.push(`selectedTargets: ${payload.selectedTargets.length}`);
|
|
1527
|
+
lines.push("");
|
|
1528
|
+
lines.push("explanation:");
|
|
1529
|
+
lines.push(
|
|
1530
|
+
` why risky: ${repositoryTopFactors.map(formatFactorSummary).join("; ") || "insufficient data"}`
|
|
1531
|
+
);
|
|
1532
|
+
lines.push(
|
|
1533
|
+
` what specifically contributed: ${repositoryTopFactors.map((factor) => `${formatFactorLabel(factor.factorId)}=${factor.contribution}`).join(", ") || "insufficient data"}`
|
|
1534
|
+
);
|
|
1535
|
+
lines.push(
|
|
1536
|
+
` dominant factors: ${repositoryTopFactors.map((factor) => formatFactorLabel(factor.factorId)).join(", ") || "insufficient data"}`
|
|
1537
|
+
);
|
|
1538
|
+
lines.push(
|
|
1539
|
+
` intersected signals: ${compositeFactors.map((factor) => `${formatFactorLabel(factor.factorId)} [${formatFactorEvidence(factor)}]`).join("; ") || "none"}`
|
|
1540
|
+
);
|
|
1541
|
+
lines.push(
|
|
1542
|
+
` what could reduce risk most: ${buildRepositoryActions(payload, repositoryTarget).join(" ")}`
|
|
1543
|
+
);
|
|
1544
|
+
lines.push("");
|
|
1545
|
+
for (const target of payload.selectedTargets) {
|
|
1546
|
+
lines.push(renderTargetText(target));
|
|
1547
|
+
lines.push("");
|
|
1548
|
+
}
|
|
1549
|
+
return lines.join("\n").trimEnd();
|
|
1550
|
+
};
|
|
1551
|
+
var renderMarkdown = (payload) => {
|
|
1552
|
+
const lines = [];
|
|
1553
|
+
const repositoryTarget = findRepositoryTarget(payload.selectedTargets) ?? findRepositoryTarget(payload.trace.targets);
|
|
1554
|
+
const repositoryTopFactors = repositoryTarget === void 0 ? [] : [...repositoryTarget.factors].sort(sortFactorByContribution).slice(0, 3);
|
|
1555
|
+
const compositeFactors = repositoryTopFactors.filter((factor) => factor.family === "composite");
|
|
1556
|
+
lines.push(`# CodeSentinel Explanation`);
|
|
1557
|
+
lines.push(`- target: \`${payload.summary.structural.targetPath}\``);
|
|
1558
|
+
lines.push(`- repositoryScore: \`${payload.summary.risk.repositoryScore}\``);
|
|
1559
|
+
lines.push(`- riskBand: \`${toRiskBand(payload.summary.risk.repositoryScore)}\``);
|
|
1560
|
+
lines.push(`- selectedTargets: \`${payload.selectedTargets.length}\``);
|
|
1561
|
+
lines.push("");
|
|
1562
|
+
lines.push(`## Summary`);
|
|
1563
|
+
lines.push(
|
|
1564
|
+
`- why risky: ${repositoryTopFactors.map(formatFactorSummary).join("; ") || "insufficient data"}`
|
|
1565
|
+
);
|
|
1566
|
+
lines.push(
|
|
1567
|
+
`- what specifically contributed: ${repositoryTopFactors.map((factor) => `${formatFactorLabel(factor.factorId)}=${factor.contribution}`).join(", ") || "insufficient data"}`
|
|
1568
|
+
);
|
|
1569
|
+
lines.push(
|
|
1570
|
+
`- dominant factors: ${repositoryTopFactors.map((factor) => formatFactorLabel(factor.factorId)).join(", ") || "insufficient data"}`
|
|
1571
|
+
);
|
|
1572
|
+
lines.push(
|
|
1573
|
+
`- intersected signals: ${compositeFactors.map((factor) => `${formatFactorLabel(factor.factorId)} [${formatFactorEvidence(factor)}]`).join("; ") || "none"}`
|
|
1574
|
+
);
|
|
1575
|
+
lines.push(
|
|
1576
|
+
`- what could reduce risk most: ${buildRepositoryActions(payload, repositoryTarget).join(" ")}`
|
|
1577
|
+
);
|
|
1578
|
+
lines.push("");
|
|
1579
|
+
for (const target of payload.selectedTargets) {
|
|
1580
|
+
lines.push(`## ${target.targetType}: \`${target.targetId}\``);
|
|
1581
|
+
lines.push(`- score: \`${target.totalScore}\` (\`${target.normalizedScore}\`)`);
|
|
1582
|
+
lines.push(`- dominantFactors: \`${target.dominantFactors.join(", ")}\``);
|
|
1583
|
+
lines.push(`- Top factors:`);
|
|
1584
|
+
for (const factor of [...target.factors].sort(sortFactorByContribution).slice(0, 5)) {
|
|
1585
|
+
lines.push(
|
|
1586
|
+
` - \`${formatFactorLabel(factor.factorId)}\` contribution=\`${factor.contribution}\` confidence=\`${factor.confidence}\``
|
|
1587
|
+
);
|
|
1588
|
+
lines.push(
|
|
1589
|
+
` - evidence: \`${formatFactorEvidence(factor)}\``
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
lines.push(`- Reduction levers:`);
|
|
1593
|
+
for (const lever of target.reductionLevers) {
|
|
1594
|
+
lines.push(
|
|
1595
|
+
` - \`${formatFactorLabel(lever.factorId)}\` estimatedImpact=\`${lever.estimatedImpact}\``
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
lines.push("");
|
|
1599
|
+
}
|
|
1600
|
+
return lines.join("\n").trimEnd();
|
|
1601
|
+
};
|
|
1602
|
+
var formatExplainOutput = (payload, format) => {
|
|
1603
|
+
if (format === "json") {
|
|
1604
|
+
return JSON.stringify(payload, null, 2);
|
|
1605
|
+
}
|
|
1606
|
+
if (format === "md") {
|
|
1607
|
+
return renderMarkdown(payload);
|
|
1608
|
+
}
|
|
1609
|
+
return renderText(payload);
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1395
1612
|
// src/application/format-dependency-risk-output.ts
|
|
1396
1613
|
var createSummaryShape2 = (result) => {
|
|
1397
1614
|
if (!result.available) {
|
|
@@ -1401,18 +1618,19 @@ var createSummaryShape2 = (result) => {
|
|
|
1401
1618
|
dependency: result.dependency
|
|
1402
1619
|
};
|
|
1403
1620
|
}
|
|
1404
|
-
const direct = result.external.dependencies[0];
|
|
1621
|
+
const direct = result.external.available ? result.external.dependencies[0] : void 0;
|
|
1405
1622
|
return {
|
|
1406
1623
|
available: true,
|
|
1407
1624
|
dependency: result.dependency,
|
|
1408
1625
|
graph: result.graph,
|
|
1409
1626
|
assumptions: result.assumptions,
|
|
1410
1627
|
external: {
|
|
1411
|
-
|
|
1628
|
+
available: result.external.available,
|
|
1629
|
+
metrics: result.external.available ? result.external.metrics : null,
|
|
1412
1630
|
ownRiskSignals: direct?.ownRiskSignals ?? [],
|
|
1413
1631
|
inheritedRiskSignals: direct?.inheritedRiskSignals ?? [],
|
|
1414
|
-
highRiskDependenciesTop: result.external.highRiskDependencies.slice(0, 10),
|
|
1415
|
-
transitiveExposureDependenciesTop: result.external.transitiveExposureDependencies.slice(0, 10)
|
|
1632
|
+
highRiskDependenciesTop: result.external.available ? result.external.highRiskDependencies.slice(0, 10) : [],
|
|
1633
|
+
transitiveExposureDependenciesTop: result.external.available ? result.external.transitiveExposureDependencies.slice(0, 10) : []
|
|
1416
1634
|
}
|
|
1417
1635
|
};
|
|
1418
1636
|
};
|
|
@@ -2696,11 +2914,86 @@ var computeDependencySignalScore = (ownSignals, inheritedSignals, inheritedSigna
|
|
|
2696
2914
|
}
|
|
2697
2915
|
return toUnitInterval(weightedTotal / maxWeightedTotal);
|
|
2698
2916
|
};
|
|
2917
|
+
var clampConfidence = (value) => round44(toUnitInterval(value));
|
|
2918
|
+
var buildFactorTraces = (totalScore, inputs) => {
|
|
2919
|
+
const positiveInputs = inputs.filter((input) => input.strength > 0);
|
|
2920
|
+
const strengthTotal = positiveInputs.reduce((sum, input) => sum + input.strength, 0);
|
|
2921
|
+
const traces = inputs.map((input) => ({
|
|
2922
|
+
factorId: input.factorId,
|
|
2923
|
+
family: input.family,
|
|
2924
|
+
contribution: 0,
|
|
2925
|
+
rawMetrics: input.rawMetrics,
|
|
2926
|
+
normalizedMetrics: input.normalizedMetrics,
|
|
2927
|
+
weight: input.weight,
|
|
2928
|
+
amplification: input.amplification,
|
|
2929
|
+
evidence: input.evidence,
|
|
2930
|
+
confidence: clampConfidence(input.confidence)
|
|
2931
|
+
}));
|
|
2932
|
+
if (strengthTotal <= 0 || totalScore <= 0) {
|
|
2933
|
+
return traces;
|
|
2934
|
+
}
|
|
2935
|
+
const scored = positiveInputs.map((input) => ({
|
|
2936
|
+
factorId: input.factorId,
|
|
2937
|
+
contribution: totalScore * input.strength / strengthTotal
|
|
2938
|
+
}));
|
|
2939
|
+
let distributed = 0;
|
|
2940
|
+
for (let index = 0; index < scored.length; index += 1) {
|
|
2941
|
+
const current = scored[index];
|
|
2942
|
+
if (current === void 0) {
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
const traceIndex = traces.findIndex((trace) => trace.factorId === current.factorId);
|
|
2946
|
+
if (traceIndex < 0) {
|
|
2947
|
+
continue;
|
|
2948
|
+
}
|
|
2949
|
+
const existing = traces[traceIndex];
|
|
2950
|
+
if (existing === void 0) {
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
if (index === scored.length - 1) {
|
|
2954
|
+
const remaining = round44(totalScore - distributed);
|
|
2955
|
+
traces[traceIndex] = {
|
|
2956
|
+
...existing,
|
|
2957
|
+
contribution: Math.max(0, remaining)
|
|
2958
|
+
};
|
|
2959
|
+
distributed += Math.max(0, remaining);
|
|
2960
|
+
continue;
|
|
2961
|
+
}
|
|
2962
|
+
const rounded = round44(current.contribution);
|
|
2963
|
+
traces[traceIndex] = {
|
|
2964
|
+
...existing,
|
|
2965
|
+
contribution: rounded
|
|
2966
|
+
};
|
|
2967
|
+
distributed += rounded;
|
|
2968
|
+
}
|
|
2969
|
+
return traces;
|
|
2970
|
+
};
|
|
2971
|
+
var buildReductionLevers = (factors) => factors.filter((factor) => factor.contribution > 0).sort(
|
|
2972
|
+
(a, b) => b.contribution - a.contribution || a.factorId.localeCompare(b.factorId)
|
|
2973
|
+
).slice(0, 3).map((factor) => ({
|
|
2974
|
+
factorId: factor.factorId,
|
|
2975
|
+
estimatedImpact: round44(factor.contribution)
|
|
2976
|
+
}));
|
|
2977
|
+
var buildTargetTrace = (targetType, targetId, totalScore, normalizedScore, factors) => {
|
|
2978
|
+
const dominantFactors = [...factors].filter((factor) => factor.contribution > 0).sort(
|
|
2979
|
+
(a, b) => b.contribution - a.contribution || a.factorId.localeCompare(b.factorId)
|
|
2980
|
+
).slice(0, 3).map((factor) => factor.factorId);
|
|
2981
|
+
return {
|
|
2982
|
+
targetType,
|
|
2983
|
+
targetId,
|
|
2984
|
+
totalScore: round44(totalScore),
|
|
2985
|
+
normalizedScore: round44(normalizedScore),
|
|
2986
|
+
factors,
|
|
2987
|
+
dominantFactors,
|
|
2988
|
+
reductionLevers: buildReductionLevers(factors)
|
|
2989
|
+
};
|
|
2990
|
+
};
|
|
2699
2991
|
var computeDependencyScores = (external, config) => {
|
|
2700
2992
|
if (!external.available) {
|
|
2701
2993
|
return {
|
|
2702
2994
|
dependencyScores: [],
|
|
2703
|
-
repositoryExternalPressure: 0
|
|
2995
|
+
repositoryExternalPressure: 0,
|
|
2996
|
+
dependencyContexts: /* @__PURE__ */ new Map()
|
|
2704
2997
|
};
|
|
2705
2998
|
}
|
|
2706
2999
|
const transitiveCounts = external.dependencies.map(
|
|
@@ -2723,6 +3016,7 @@ var computeDependencyScores = (external, config) => {
|
|
|
2723
3016
|
config.quantileClamp.lower,
|
|
2724
3017
|
config.quantileClamp.upper
|
|
2725
3018
|
);
|
|
3019
|
+
const dependencyContexts = /* @__PURE__ */ new Map();
|
|
2726
3020
|
const dependencyScores = external.dependencies.map((dependency) => {
|
|
2727
3021
|
const signalScore = computeDependencySignalScore(
|
|
2728
3022
|
dependency.ownRiskSignals,
|
|
@@ -2751,6 +3045,33 @@ var computeDependencyScores = (external, config) => {
|
|
|
2751
3045
|
config.dependencySignals.popularityHalfLifeDownloads
|
|
2752
3046
|
) * config.dependencySignals.popularityMaxDampening;
|
|
2753
3047
|
const normalizedScore = toUnitInterval(baseScore * popularityDampener);
|
|
3048
|
+
const availableMetricCount = [
|
|
3049
|
+
dependency.daysSinceLastRelease,
|
|
3050
|
+
dependency.maintainerCount,
|
|
3051
|
+
dependency.busFactor,
|
|
3052
|
+
dependency.weeklyDownloads
|
|
3053
|
+
].filter((value) => value !== null).length;
|
|
3054
|
+
const confidence = toUnitInterval(0.5 + availableMetricCount * 0.125);
|
|
3055
|
+
dependencyContexts.set(dependency.name, {
|
|
3056
|
+
signalScore: round44(signalScore),
|
|
3057
|
+
stalenessRisk: round44(stalenessRisk),
|
|
3058
|
+
maintainerConcentrationRisk: round44(maintainerConcentrationRisk),
|
|
3059
|
+
transitiveBurdenRisk: round44(transitiveBurdenRisk),
|
|
3060
|
+
centralityRisk: round44(centralityRisk),
|
|
3061
|
+
chainDepthRisk: round44(chainDepthRisk),
|
|
3062
|
+
busFactorRisk: round44(busFactorRisk),
|
|
3063
|
+
popularityDampener: round44(popularityDampener),
|
|
3064
|
+
rawMetrics: {
|
|
3065
|
+
daysSinceLastRelease: dependency.daysSinceLastRelease,
|
|
3066
|
+
maintainerCount: dependency.maintainerCount,
|
|
3067
|
+
transitiveCount: dependency.transitiveDependencies.length,
|
|
3068
|
+
dependents: dependency.dependents,
|
|
3069
|
+
dependencyDepth: dependency.dependencyDepth,
|
|
3070
|
+
busFactor: dependency.busFactor,
|
|
3071
|
+
weeklyDownloads: dependency.weeklyDownloads
|
|
3072
|
+
},
|
|
3073
|
+
confidence: round44(confidence)
|
|
3074
|
+
});
|
|
2754
3075
|
return {
|
|
2755
3076
|
dependency: dependency.name,
|
|
2756
3077
|
score: round44(normalizedScore * 100),
|
|
@@ -2773,7 +3094,8 @@ var computeDependencyScores = (external, config) => {
|
|
|
2773
3094
|
);
|
|
2774
3095
|
return {
|
|
2775
3096
|
dependencyScores,
|
|
2776
|
-
repositoryExternalPressure: round44(repositoryExternalPressure)
|
|
3097
|
+
repositoryExternalPressure: round44(repositoryExternalPressure),
|
|
3098
|
+
dependencyContexts
|
|
2777
3099
|
};
|
|
2778
3100
|
};
|
|
2779
3101
|
var mapEvolutionByFile = (evolution) => {
|
|
@@ -2925,7 +3247,8 @@ var buildFragileClusters = (structural, evolution, fileScoresByFile, config) =>
|
|
|
2925
3247
|
(a, b) => b.score - a.score || a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)
|
|
2926
3248
|
);
|
|
2927
3249
|
};
|
|
2928
|
-
var computeRiskSummary = (structural, evolution, external, config) => {
|
|
3250
|
+
var computeRiskSummary = (structural, evolution, external, config, traceCollector) => {
|
|
3251
|
+
const collector = traceCollector;
|
|
2929
3252
|
const dependencyComputation = computeDependencyScores(external, config);
|
|
2930
3253
|
const evolutionByFile = mapEvolutionByFile(evolution);
|
|
2931
3254
|
const evolutionScales = computeEvolutionScales(evolutionByFile, config);
|
|
@@ -2964,19 +3287,20 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2964
3287
|
);
|
|
2965
3288
|
const structuralCentrality = toUnitInterval((fanInRisk + fanOutRisk) / 2);
|
|
2966
3289
|
let evolutionFactor = 0;
|
|
3290
|
+
let frequencyRisk = 0;
|
|
3291
|
+
let churnRisk = 0;
|
|
3292
|
+
let volatilityRisk = 0;
|
|
3293
|
+
let ownershipConcentrationRisk = 0;
|
|
3294
|
+
let busFactorRisk = 0;
|
|
2967
3295
|
const evolutionMetrics = evolutionByFile.get(filePath);
|
|
2968
3296
|
if (evolution.available && evolutionMetrics !== void 0) {
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
3297
|
+
frequencyRisk = normalizeWithScale(logScale(evolutionMetrics.commitCount), evolutionScales.commitCount);
|
|
3298
|
+
churnRisk = normalizeWithScale(logScale(evolutionMetrics.churnTotal), evolutionScales.churnTotal);
|
|
3299
|
+
volatilityRisk = toUnitInterval(evolutionMetrics.recentVolatility);
|
|
3300
|
+
ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShare);
|
|
3301
|
+
busFactorRisk = toUnitInterval(
|
|
3302
|
+
1 - normalizeWithScale(evolutionMetrics.busFactor, evolutionScales.busFactor)
|
|
2972
3303
|
);
|
|
2973
|
-
const churnRisk = normalizeWithScale(
|
|
2974
|
-
logScale(evolutionMetrics.churnTotal),
|
|
2975
|
-
evolutionScales.churnTotal
|
|
2976
|
-
);
|
|
2977
|
-
const volatilityRisk = toUnitInterval(evolutionMetrics.recentVolatility);
|
|
2978
|
-
const ownershipConcentrationRisk = toUnitInterval(evolutionMetrics.topAuthorShare);
|
|
2979
|
-
const busFactorRisk = toUnitInterval(1 - normalizeWithScale(evolutionMetrics.busFactor, evolutionScales.busFactor));
|
|
2980
3304
|
const evolutionWeights = config.evolutionFactorWeights;
|
|
2981
3305
|
evolutionFactor = toUnitInterval(
|
|
2982
3306
|
frequencyRisk * evolutionWeights.frequency + churnRisk * evolutionWeights.churn + volatilityRisk * evolutionWeights.recentVolatility + ownershipConcentrationRisk * evolutionWeights.ownershipConcentration + busFactorRisk * evolutionWeights.busFactorRisk
|
|
@@ -2984,11 +3308,17 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
2984
3308
|
}
|
|
2985
3309
|
const dependencyAffinity = toUnitInterval(structuralCentrality * 0.6 + evolutionFactor * 0.4);
|
|
2986
3310
|
const externalFactor = external.available ? toUnitInterval(dependencyComputation.repositoryExternalPressure * dependencyAffinity) : 0;
|
|
2987
|
-
const
|
|
3311
|
+
const structuralBase = structuralFactor * dimensionWeights.structural;
|
|
3312
|
+
const evolutionBase = evolutionFactor * dimensionWeights.evolution;
|
|
3313
|
+
const externalBase = externalFactor * dimensionWeights.external;
|
|
3314
|
+
const baseline = structuralBase + evolutionBase + externalBase;
|
|
3315
|
+
const interactionStructuralEvolution = structuralFactor * evolutionFactor * config.interactionWeights.structuralEvolution;
|
|
3316
|
+
const interactionCentralInstability = structuralCentrality * evolutionFactor * config.interactionWeights.centralInstability;
|
|
3317
|
+
const interactionDependencyAmplification = externalFactor * Math.max(structuralFactor, evolutionFactor) * config.interactionWeights.dependencyAmplification;
|
|
2988
3318
|
const interactions = [
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
3319
|
+
interactionStructuralEvolution,
|
|
3320
|
+
interactionCentralInstability,
|
|
3321
|
+
interactionDependencyAmplification
|
|
2992
3322
|
];
|
|
2993
3323
|
const normalizedScore = saturatingComposite(baseline, interactions);
|
|
2994
3324
|
return {
|
|
@@ -3000,7 +3330,38 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
3000
3330
|
evolution: round44(evolutionFactor),
|
|
3001
3331
|
external: round44(externalFactor)
|
|
3002
3332
|
},
|
|
3003
|
-
structuralCentrality: round44(structuralCentrality)
|
|
3333
|
+
structuralCentrality: round44(structuralCentrality),
|
|
3334
|
+
traceTerms: {
|
|
3335
|
+
structuralBase: round44(structuralBase),
|
|
3336
|
+
evolutionBase: round44(evolutionBase),
|
|
3337
|
+
externalBase: round44(externalBase),
|
|
3338
|
+
interactionStructuralEvolution: round44(interactionStructuralEvolution),
|
|
3339
|
+
interactionCentralInstability: round44(interactionCentralInstability),
|
|
3340
|
+
interactionDependencyAmplification: round44(interactionDependencyAmplification)
|
|
3341
|
+
},
|
|
3342
|
+
rawMetrics: {
|
|
3343
|
+
fanIn: file.fanIn,
|
|
3344
|
+
fanOut: file.fanOut,
|
|
3345
|
+
depth: file.depth,
|
|
3346
|
+
cycleParticipation: inCycle,
|
|
3347
|
+
commitCount: evolutionMetrics?.commitCount ?? null,
|
|
3348
|
+
churnTotal: evolutionMetrics?.churnTotal ?? null,
|
|
3349
|
+
recentVolatility: evolutionMetrics?.recentVolatility ?? null,
|
|
3350
|
+
topAuthorShare: evolutionMetrics?.topAuthorShare ?? null,
|
|
3351
|
+
busFactor: evolutionMetrics?.busFactor ?? null,
|
|
3352
|
+
dependencyAffinity: round44(dependencyAffinity),
|
|
3353
|
+
repositoryExternalPressure: round44(dependencyComputation.repositoryExternalPressure)
|
|
3354
|
+
},
|
|
3355
|
+
normalizedMetrics: {
|
|
3356
|
+
fanInRisk: round44(fanInRisk),
|
|
3357
|
+
fanOutRisk: round44(fanOutRisk),
|
|
3358
|
+
depthRisk: round44(depthRisk),
|
|
3359
|
+
frequencyRisk: round44(frequencyRisk),
|
|
3360
|
+
churnRisk: round44(churnRisk),
|
|
3361
|
+
volatilityRisk: round44(volatilityRisk),
|
|
3362
|
+
ownershipConcentrationRisk: round44(ownershipConcentrationRisk),
|
|
3363
|
+
busFactorRisk: round44(busFactorRisk)
|
|
3364
|
+
}
|
|
3004
3365
|
};
|
|
3005
3366
|
}).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
3006
3367
|
const fileScores = fileRiskContexts.map((context) => ({
|
|
@@ -3009,6 +3370,103 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
3009
3370
|
normalizedScore: context.normalizedScore,
|
|
3010
3371
|
factors: context.factors
|
|
3011
3372
|
}));
|
|
3373
|
+
if (collector !== void 0) {
|
|
3374
|
+
for (const context of fileRiskContexts) {
|
|
3375
|
+
const evidence = [
|
|
3376
|
+
{ kind: "file_metric", target: context.file, metric: "fanIn" },
|
|
3377
|
+
{ kind: "file_metric", target: context.file, metric: "fanOut" },
|
|
3378
|
+
{ kind: "file_metric", target: context.file, metric: "depth" }
|
|
3379
|
+
];
|
|
3380
|
+
if (context.rawMetrics.cycleParticipation > 0) {
|
|
3381
|
+
evidence.push({
|
|
3382
|
+
kind: "graph_cycle",
|
|
3383
|
+
cycleId: `file:${context.file}`,
|
|
3384
|
+
files: [context.file]
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
const fileFactors = buildFactorTraces(context.score, [
|
|
3388
|
+
{
|
|
3389
|
+
factorId: "file.structural",
|
|
3390
|
+
family: "structural",
|
|
3391
|
+
strength: context.traceTerms.structuralBase,
|
|
3392
|
+
rawMetrics: {
|
|
3393
|
+
fanIn: context.rawMetrics.fanIn,
|
|
3394
|
+
fanOut: context.rawMetrics.fanOut,
|
|
3395
|
+
depth: context.rawMetrics.depth,
|
|
3396
|
+
cycleParticipation: context.rawMetrics.cycleParticipation
|
|
3397
|
+
},
|
|
3398
|
+
normalizedMetrics: {
|
|
3399
|
+
fanInRisk: context.normalizedMetrics.fanInRisk,
|
|
3400
|
+
fanOutRisk: context.normalizedMetrics.fanOutRisk,
|
|
3401
|
+
depthRisk: context.normalizedMetrics.depthRisk,
|
|
3402
|
+
structuralFactor: context.factors.structural
|
|
3403
|
+
},
|
|
3404
|
+
weight: dimensionWeights.structural,
|
|
3405
|
+
amplification: null,
|
|
3406
|
+
evidence,
|
|
3407
|
+
confidence: 1
|
|
3408
|
+
},
|
|
3409
|
+
{
|
|
3410
|
+
factorId: "file.evolution",
|
|
3411
|
+
family: "evolution",
|
|
3412
|
+
strength: context.traceTerms.evolutionBase,
|
|
3413
|
+
rawMetrics: {
|
|
3414
|
+
commitCount: context.rawMetrics.commitCount,
|
|
3415
|
+
churnTotal: context.rawMetrics.churnTotal,
|
|
3416
|
+
recentVolatility: context.rawMetrics.recentVolatility,
|
|
3417
|
+
topAuthorShare: context.rawMetrics.topAuthorShare,
|
|
3418
|
+
busFactor: context.rawMetrics.busFactor
|
|
3419
|
+
},
|
|
3420
|
+
normalizedMetrics: {
|
|
3421
|
+
frequencyRisk: context.normalizedMetrics.frequencyRisk,
|
|
3422
|
+
churnRisk: context.normalizedMetrics.churnRisk,
|
|
3423
|
+
volatilityRisk: context.normalizedMetrics.volatilityRisk,
|
|
3424
|
+
ownershipConcentrationRisk: context.normalizedMetrics.ownershipConcentrationRisk,
|
|
3425
|
+
busFactorRisk: context.normalizedMetrics.busFactorRisk,
|
|
3426
|
+
evolutionFactor: context.factors.evolution
|
|
3427
|
+
},
|
|
3428
|
+
weight: dimensionWeights.evolution,
|
|
3429
|
+
amplification: null,
|
|
3430
|
+
evidence: [{ kind: "file_metric", target: context.file, metric: "commitCount" }],
|
|
3431
|
+
confidence: evolution.available ? 1 : 0
|
|
3432
|
+
},
|
|
3433
|
+
{
|
|
3434
|
+
factorId: "file.external",
|
|
3435
|
+
family: "external",
|
|
3436
|
+
strength: context.traceTerms.externalBase,
|
|
3437
|
+
rawMetrics: {
|
|
3438
|
+
repositoryExternalPressure: context.rawMetrics.repositoryExternalPressure,
|
|
3439
|
+
dependencyAffinity: context.rawMetrics.dependencyAffinity
|
|
3440
|
+
},
|
|
3441
|
+
normalizedMetrics: {
|
|
3442
|
+
externalFactor: context.factors.external
|
|
3443
|
+
},
|
|
3444
|
+
weight: dimensionWeights.external,
|
|
3445
|
+
amplification: null,
|
|
3446
|
+
evidence: [{ kind: "repository_metric", metric: "repositoryExternalPressure" }],
|
|
3447
|
+
confidence: external.available ? 0.7 : 0
|
|
3448
|
+
},
|
|
3449
|
+
{
|
|
3450
|
+
factorId: "file.composite.interactions",
|
|
3451
|
+
family: "composite",
|
|
3452
|
+
strength: context.traceTerms.interactionStructuralEvolution + context.traceTerms.interactionCentralInstability + context.traceTerms.interactionDependencyAmplification,
|
|
3453
|
+
rawMetrics: {
|
|
3454
|
+
structuralEvolutionInteraction: context.traceTerms.interactionStructuralEvolution,
|
|
3455
|
+
centralInstabilityInteraction: context.traceTerms.interactionCentralInstability,
|
|
3456
|
+
dependencyAmplificationInteraction: context.traceTerms.interactionDependencyAmplification
|
|
3457
|
+
},
|
|
3458
|
+
normalizedMetrics: {},
|
|
3459
|
+
weight: null,
|
|
3460
|
+
amplification: config.interactionWeights.structuralEvolution + config.interactionWeights.centralInstability + config.interactionWeights.dependencyAmplification,
|
|
3461
|
+
evidence: [{ kind: "repository_metric", metric: "interactionWeights" }],
|
|
3462
|
+
confidence: 0.9
|
|
3463
|
+
}
|
|
3464
|
+
]);
|
|
3465
|
+
collector.record(
|
|
3466
|
+
buildTargetTrace("file", context.file, context.score, context.normalizedScore, fileFactors)
|
|
3467
|
+
);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3012
3470
|
const fileScoresByFile = new Map(fileScores.map((fileScore) => [fileScore.file, fileScore]));
|
|
3013
3471
|
const hotspotsCount = Math.min(
|
|
3014
3472
|
config.hotspotMaxFiles,
|
|
@@ -3037,6 +3495,39 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
3037
3495
|
fileCount: values.length
|
|
3038
3496
|
};
|
|
3039
3497
|
}).sort((a, b) => b.score - a.score || a.module.localeCompare(b.module));
|
|
3498
|
+
if (collector !== void 0) {
|
|
3499
|
+
for (const [module, values] of moduleFiles.entries()) {
|
|
3500
|
+
const averageScore = average(values);
|
|
3501
|
+
const peakScore = values.reduce((max, value) => Math.max(max, value), 0);
|
|
3502
|
+
const normalizedScore = toUnitInterval(averageScore * 0.65 + peakScore * 0.35);
|
|
3503
|
+
const totalScore = round44(normalizedScore * 100);
|
|
3504
|
+
const factors = buildFactorTraces(totalScore, [
|
|
3505
|
+
{
|
|
3506
|
+
factorId: "module.average_file_risk",
|
|
3507
|
+
family: "composite",
|
|
3508
|
+
strength: averageScore * 0.65,
|
|
3509
|
+
rawMetrics: { averageFileRisk: round44(averageScore), fileCount: values.length },
|
|
3510
|
+
normalizedMetrics: { normalizedModuleRisk: round44(normalizedScore) },
|
|
3511
|
+
weight: 0.65,
|
|
3512
|
+
amplification: null,
|
|
3513
|
+
evidence: [{ kind: "repository_metric", metric: "moduleAggregation.average" }],
|
|
3514
|
+
confidence: 1
|
|
3515
|
+
},
|
|
3516
|
+
{
|
|
3517
|
+
factorId: "module.peak_file_risk",
|
|
3518
|
+
family: "composite",
|
|
3519
|
+
strength: peakScore * 0.35,
|
|
3520
|
+
rawMetrics: { peakFileRisk: round44(peakScore), fileCount: values.length },
|
|
3521
|
+
normalizedMetrics: { normalizedModuleRisk: round44(normalizedScore) },
|
|
3522
|
+
weight: 0.35,
|
|
3523
|
+
amplification: null,
|
|
3524
|
+
evidence: [{ kind: "repository_metric", metric: "moduleAggregation.peak" }],
|
|
3525
|
+
confidence: 1
|
|
3526
|
+
}
|
|
3527
|
+
]);
|
|
3528
|
+
collector.record(buildTargetTrace("module", module, totalScore, normalizedScore, factors));
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3040
3531
|
const fragileClusters = buildFragileClusters(structural, evolution, fileScoresByFile, config);
|
|
3041
3532
|
const externalPressures = fileScores.map((fileScore) => fileScore.factors.external);
|
|
3042
3533
|
const pressureThreshold = Math.max(
|
|
@@ -3057,6 +3548,107 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
3057
3548
|
...zone,
|
|
3058
3549
|
externalPressure: round44(zone.externalPressure)
|
|
3059
3550
|
}));
|
|
3551
|
+
if (collector !== void 0 && external.available) {
|
|
3552
|
+
const dependencyByName = new Map(external.dependencies.map((dependency) => [dependency.name, dependency]));
|
|
3553
|
+
for (const dependencyScore of dependencyComputation.dependencyScores) {
|
|
3554
|
+
const dependency = dependencyByName.get(dependencyScore.dependency);
|
|
3555
|
+
const context = dependencyComputation.dependencyContexts.get(dependencyScore.dependency);
|
|
3556
|
+
if (dependency === void 0 || context === void 0) {
|
|
3557
|
+
continue;
|
|
3558
|
+
}
|
|
3559
|
+
const hasMetadata = context.rawMetrics.daysSinceLastRelease !== null && context.rawMetrics.maintainerCount !== null;
|
|
3560
|
+
const factors = buildFactorTraces(dependencyScore.score, [
|
|
3561
|
+
{
|
|
3562
|
+
factorId: "dependency.signals",
|
|
3563
|
+
family: "external",
|
|
3564
|
+
strength: context.signalScore * config.dependencyFactorWeights.signals,
|
|
3565
|
+
rawMetrics: {
|
|
3566
|
+
ownSignals: dependency.ownRiskSignals.length,
|
|
3567
|
+
inheritedSignals: dependency.inheritedRiskSignals.length
|
|
3568
|
+
},
|
|
3569
|
+
normalizedMetrics: { signalScore: context.signalScore },
|
|
3570
|
+
weight: config.dependencyFactorWeights.signals,
|
|
3571
|
+
amplification: config.dependencySignals.inheritedSignalMultiplier,
|
|
3572
|
+
evidence: [{ kind: "dependency_metric", target: dependency.name, metric: "riskSignals" }],
|
|
3573
|
+
confidence: 0.95
|
|
3574
|
+
},
|
|
3575
|
+
{
|
|
3576
|
+
factorId: "dependency.staleness",
|
|
3577
|
+
family: "external",
|
|
3578
|
+
strength: context.stalenessRisk * config.dependencyFactorWeights.staleness,
|
|
3579
|
+
rawMetrics: { daysSinceLastRelease: context.rawMetrics.daysSinceLastRelease },
|
|
3580
|
+
normalizedMetrics: { stalenessRisk: context.stalenessRisk },
|
|
3581
|
+
weight: config.dependencyFactorWeights.staleness,
|
|
3582
|
+
amplification: null,
|
|
3583
|
+
evidence: [{ kind: "dependency_metric", target: dependency.name, metric: "daysSinceLastRelease" }],
|
|
3584
|
+
confidence: hasMetadata ? 0.9 : 0.5
|
|
3585
|
+
},
|
|
3586
|
+
{
|
|
3587
|
+
factorId: "dependency.maintainer_concentration",
|
|
3588
|
+
family: "external",
|
|
3589
|
+
strength: context.maintainerConcentrationRisk * config.dependencyFactorWeights.maintainerConcentration,
|
|
3590
|
+
rawMetrics: { maintainerCount: context.rawMetrics.maintainerCount },
|
|
3591
|
+
normalizedMetrics: {
|
|
3592
|
+
maintainerConcentrationRisk: context.maintainerConcentrationRisk
|
|
3593
|
+
},
|
|
3594
|
+
weight: config.dependencyFactorWeights.maintainerConcentration,
|
|
3595
|
+
amplification: null,
|
|
3596
|
+
evidence: [{ kind: "dependency_metric", target: dependency.name, metric: "maintainerCount" }],
|
|
3597
|
+
confidence: hasMetadata ? 0.9 : 0.5
|
|
3598
|
+
},
|
|
3599
|
+
{
|
|
3600
|
+
factorId: "dependency.topology",
|
|
3601
|
+
family: "external",
|
|
3602
|
+
strength: context.transitiveBurdenRisk * config.dependencyFactorWeights.transitiveBurden + context.centralityRisk * config.dependencyFactorWeights.centrality + context.chainDepthRisk * config.dependencyFactorWeights.chainDepth,
|
|
3603
|
+
rawMetrics: {
|
|
3604
|
+
transitiveCount: context.rawMetrics.transitiveCount,
|
|
3605
|
+
dependents: context.rawMetrics.dependents,
|
|
3606
|
+
dependencyDepth: context.rawMetrics.dependencyDepth
|
|
3607
|
+
},
|
|
3608
|
+
normalizedMetrics: {
|
|
3609
|
+
transitiveBurdenRisk: context.transitiveBurdenRisk,
|
|
3610
|
+
centralityRisk: context.centralityRisk,
|
|
3611
|
+
chainDepthRisk: context.chainDepthRisk
|
|
3612
|
+
},
|
|
3613
|
+
weight: config.dependencyFactorWeights.transitiveBurden + config.dependencyFactorWeights.centrality + config.dependencyFactorWeights.chainDepth,
|
|
3614
|
+
amplification: null,
|
|
3615
|
+
evidence: [{ kind: "dependency_metric", target: dependency.name, metric: "dependencyDepth" }],
|
|
3616
|
+
confidence: 1
|
|
3617
|
+
},
|
|
3618
|
+
{
|
|
3619
|
+
factorId: "dependency.bus_factor",
|
|
3620
|
+
family: "external",
|
|
3621
|
+
strength: context.busFactorRisk * config.dependencyFactorWeights.busFactorRisk,
|
|
3622
|
+
rawMetrics: { busFactor: context.rawMetrics.busFactor },
|
|
3623
|
+
normalizedMetrics: { busFactorRisk: context.busFactorRisk },
|
|
3624
|
+
weight: config.dependencyFactorWeights.busFactorRisk,
|
|
3625
|
+
amplification: null,
|
|
3626
|
+
evidence: [{ kind: "dependency_metric", target: dependency.name, metric: "busFactor" }],
|
|
3627
|
+
confidence: context.rawMetrics.busFactor === null ? 0.5 : 0.85
|
|
3628
|
+
},
|
|
3629
|
+
{
|
|
3630
|
+
factorId: "dependency.popularity_dampening",
|
|
3631
|
+
family: "composite",
|
|
3632
|
+
strength: 1 - context.popularityDampener,
|
|
3633
|
+
rawMetrics: { weeklyDownloads: context.rawMetrics.weeklyDownloads },
|
|
3634
|
+
normalizedMetrics: { popularityDampener: context.popularityDampener },
|
|
3635
|
+
weight: config.dependencySignals.popularityMaxDampening,
|
|
3636
|
+
amplification: null,
|
|
3637
|
+
evidence: [{ kind: "dependency_metric", target: dependency.name, metric: "weeklyDownloads" }],
|
|
3638
|
+
confidence: context.rawMetrics.weeklyDownloads === null ? 0.4 : 0.9
|
|
3639
|
+
}
|
|
3640
|
+
]);
|
|
3641
|
+
collector.record(
|
|
3642
|
+
buildTargetTrace(
|
|
3643
|
+
"dependency",
|
|
3644
|
+
dependencyScore.dependency,
|
|
3645
|
+
dependencyScore.score,
|
|
3646
|
+
dependencyScore.normalizedScore,
|
|
3647
|
+
factors
|
|
3648
|
+
)
|
|
3649
|
+
);
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3060
3652
|
const structuralDimension = average(fileScores.map((fileScore) => fileScore.factors.structural));
|
|
3061
3653
|
const evolutionDimension = average(fileScores.map((fileScore) => fileScore.factors.evolution));
|
|
3062
3654
|
const externalDimension = dependencyComputation.repositoryExternalPressure;
|
|
@@ -3077,8 +3669,79 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
3077
3669
|
criticalInstability * config.interactionWeights.centralInstability,
|
|
3078
3670
|
dependencyAmplification * config.interactionWeights.dependencyAmplification
|
|
3079
3671
|
]);
|
|
3672
|
+
const repositoryScore = round44(repositoryNormalizedScore * 100);
|
|
3673
|
+
if (collector !== void 0) {
|
|
3674
|
+
const repositoryFactors = buildFactorTraces(repositoryScore, [
|
|
3675
|
+
{
|
|
3676
|
+
factorId: "repository.structural",
|
|
3677
|
+
family: "structural",
|
|
3678
|
+
strength: structuralDimension * dimensionWeights.structural,
|
|
3679
|
+
rawMetrics: { structuralDimension: round44(structuralDimension) },
|
|
3680
|
+
normalizedMetrics: { dimensionWeight: round44(dimensionWeights.structural) },
|
|
3681
|
+
weight: dimensionWeights.structural,
|
|
3682
|
+
amplification: null,
|
|
3683
|
+
evidence: [{ kind: "repository_metric", metric: "structuralDimension" }],
|
|
3684
|
+
confidence: 1
|
|
3685
|
+
},
|
|
3686
|
+
{
|
|
3687
|
+
factorId: "repository.evolution",
|
|
3688
|
+
family: "evolution",
|
|
3689
|
+
strength: evolutionDimension * dimensionWeights.evolution,
|
|
3690
|
+
rawMetrics: { evolutionDimension: round44(evolutionDimension) },
|
|
3691
|
+
normalizedMetrics: { dimensionWeight: round44(dimensionWeights.evolution) },
|
|
3692
|
+
weight: dimensionWeights.evolution,
|
|
3693
|
+
amplification: null,
|
|
3694
|
+
evidence: [{ kind: "repository_metric", metric: "evolutionDimension" }],
|
|
3695
|
+
confidence: evolution.available ? 1 : 0
|
|
3696
|
+
},
|
|
3697
|
+
{
|
|
3698
|
+
factorId: "repository.external",
|
|
3699
|
+
family: "external",
|
|
3700
|
+
strength: externalDimension * dimensionWeights.external,
|
|
3701
|
+
rawMetrics: { externalDimension: round44(externalDimension) },
|
|
3702
|
+
normalizedMetrics: { dimensionWeight: round44(dimensionWeights.external) },
|
|
3703
|
+
weight: dimensionWeights.external,
|
|
3704
|
+
amplification: null,
|
|
3705
|
+
evidence: [{ kind: "repository_metric", metric: "externalDimension" }],
|
|
3706
|
+
confidence: external.available ? 0.8 : 0
|
|
3707
|
+
},
|
|
3708
|
+
{
|
|
3709
|
+
factorId: "repository.composite.interactions",
|
|
3710
|
+
family: "composite",
|
|
3711
|
+
strength: structuralDimension * evolutionDimension * config.interactionWeights.structuralEvolution + criticalInstability * config.interactionWeights.centralInstability + dependencyAmplification * config.interactionWeights.dependencyAmplification,
|
|
3712
|
+
rawMetrics: {
|
|
3713
|
+
structuralEvolution: round44(
|
|
3714
|
+
structuralDimension * evolutionDimension * config.interactionWeights.structuralEvolution
|
|
3715
|
+
),
|
|
3716
|
+
centralInstability: round44(
|
|
3717
|
+
criticalInstability * config.interactionWeights.centralInstability
|
|
3718
|
+
),
|
|
3719
|
+
dependencyAmplification: round44(
|
|
3720
|
+
dependencyAmplification * config.interactionWeights.dependencyAmplification
|
|
3721
|
+
)
|
|
3722
|
+
},
|
|
3723
|
+
normalizedMetrics: {
|
|
3724
|
+
criticalInstability: round44(criticalInstability),
|
|
3725
|
+
dependencyAmplification: round44(dependencyAmplification)
|
|
3726
|
+
},
|
|
3727
|
+
weight: null,
|
|
3728
|
+
amplification: config.interactionWeights.structuralEvolution + config.interactionWeights.centralInstability + config.interactionWeights.dependencyAmplification,
|
|
3729
|
+
evidence: [{ kind: "repository_metric", metric: "interactionTerms" }],
|
|
3730
|
+
confidence: 0.9
|
|
3731
|
+
}
|
|
3732
|
+
]);
|
|
3733
|
+
collector.record(
|
|
3734
|
+
buildTargetTrace(
|
|
3735
|
+
"repository",
|
|
3736
|
+
structural.targetPath,
|
|
3737
|
+
repositoryScore,
|
|
3738
|
+
repositoryNormalizedScore,
|
|
3739
|
+
repositoryFactors
|
|
3740
|
+
)
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3080
3743
|
return {
|
|
3081
|
-
repositoryScore
|
|
3744
|
+
repositoryScore,
|
|
3082
3745
|
normalizedScore: round44(repositoryNormalizedScore),
|
|
3083
3746
|
hotspots,
|
|
3084
3747
|
fragileClusters,
|
|
@@ -3088,6 +3751,37 @@ var computeRiskSummary = (structural, evolution, external, config) => {
|
|
|
3088
3751
|
dependencyScores: dependencyComputation.dependencyScores
|
|
3089
3752
|
};
|
|
3090
3753
|
};
|
|
3754
|
+
var NoopTraceCollector = class {
|
|
3755
|
+
record(_target) {
|
|
3756
|
+
}
|
|
3757
|
+
build() {
|
|
3758
|
+
return void 0;
|
|
3759
|
+
}
|
|
3760
|
+
};
|
|
3761
|
+
var RecordingTraceCollector = class {
|
|
3762
|
+
targets = [];
|
|
3763
|
+
record(target) {
|
|
3764
|
+
this.targets.push(target);
|
|
3765
|
+
}
|
|
3766
|
+
build() {
|
|
3767
|
+
const orderedTargets = [...this.targets].sort((a, b) => {
|
|
3768
|
+
if (a.targetType !== b.targetType) {
|
|
3769
|
+
return a.targetType.localeCompare(b.targetType);
|
|
3770
|
+
}
|
|
3771
|
+
if (b.totalScore !== a.totalScore) {
|
|
3772
|
+
return b.totalScore - a.totalScore;
|
|
3773
|
+
}
|
|
3774
|
+
return a.targetId.localeCompare(b.targetId);
|
|
3775
|
+
});
|
|
3776
|
+
return {
|
|
3777
|
+
schemaVersion: "1",
|
|
3778
|
+
contributionTolerance: 1e-4,
|
|
3779
|
+
targets: orderedTargets
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
};
|
|
3783
|
+
var noopCollectorSingleton = new NoopTraceCollector();
|
|
3784
|
+
var createTraceCollector = (enabled) => enabled ? new RecordingTraceCollector() : noopCollectorSingleton;
|
|
3091
3785
|
var mergeConfig = (overrides) => {
|
|
3092
3786
|
if (overrides === void 0) {
|
|
3093
3787
|
return DEFAULT_RISK_ENGINE_CONFIG;
|
|
@@ -3142,8 +3836,29 @@ var mergeConfig = (overrides) => {
|
|
|
3142
3836
|
};
|
|
3143
3837
|
};
|
|
3144
3838
|
var computeRepositoryRiskSummary = (input) => {
|
|
3839
|
+
return evaluateRepositoryRisk(input, { explain: false }).summary;
|
|
3840
|
+
};
|
|
3841
|
+
var evaluateRepositoryRisk = (input, options = {}) => {
|
|
3145
3842
|
const config = mergeConfig(input.config);
|
|
3146
|
-
|
|
3843
|
+
const collector = createTraceCollector(options.explain === true);
|
|
3844
|
+
const summary = computeRiskSummary(
|
|
3845
|
+
input.structural,
|
|
3846
|
+
input.evolution,
|
|
3847
|
+
input.external,
|
|
3848
|
+
config,
|
|
3849
|
+
collector
|
|
3850
|
+
);
|
|
3851
|
+
const trace = collector.build();
|
|
3852
|
+
if (options.explain !== true) {
|
|
3853
|
+
return { summary };
|
|
3854
|
+
}
|
|
3855
|
+
if (trace === void 0) {
|
|
3856
|
+
return { summary };
|
|
3857
|
+
}
|
|
3858
|
+
return {
|
|
3859
|
+
summary,
|
|
3860
|
+
trace
|
|
3861
|
+
};
|
|
3147
3862
|
};
|
|
3148
3863
|
|
|
3149
3864
|
// src/application/run-analyze-command.ts
|
|
@@ -3259,7 +3974,7 @@ var createEvolutionProgressReporter = (logger) => {
|
|
|
3259
3974
|
}
|
|
3260
3975
|
};
|
|
3261
3976
|
};
|
|
3262
|
-
var
|
|
3977
|
+
var collectAnalysisInputs = async (inputPath, authorIdentityMode, logger = createSilentLogger()) => {
|
|
3263
3978
|
const invocationCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
3264
3979
|
const targetPath = resolveTargetPath(inputPath, invocationCwd);
|
|
3265
3980
|
logger.info(`analyzing repository: ${targetPath}`);
|
|
@@ -3272,10 +3987,13 @@ var runAnalyzeCommand = async (inputPath, authorIdentityMode, logger = createSil
|
|
|
3272
3987
|
`structural metrics: nodes=${structural.metrics.nodeCount}, edges=${structural.metrics.edgeCount}, cycles=${structural.metrics.cycleCount}`
|
|
3273
3988
|
);
|
|
3274
3989
|
logger.info(`analyzing git evolution (author identity: ${authorIdentityMode})`);
|
|
3275
|
-
const evolution = analyzeRepositoryEvolutionFromGit(
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3990
|
+
const evolution = analyzeRepositoryEvolutionFromGit(
|
|
3991
|
+
{
|
|
3992
|
+
repositoryPath: targetPath,
|
|
3993
|
+
config: { authorIdentityMode }
|
|
3994
|
+
},
|
|
3995
|
+
createEvolutionProgressReporter(logger)
|
|
3996
|
+
);
|
|
3279
3997
|
if (evolution.available) {
|
|
3280
3998
|
logger.debug(
|
|
3281
3999
|
`evolution metrics: commits=${evolution.metrics.totalCommits}, files=${evolution.metrics.totalFiles}, hotspotThreshold=${evolution.metrics.hotspotThresholdCommitCount}`
|
|
@@ -3295,20 +4013,60 @@ var runAnalyzeCommand = async (inputPath, authorIdentityMode, logger = createSil
|
|
|
3295
4013
|
} else {
|
|
3296
4014
|
logger.warn(`external analysis unavailable: ${external.reason}`);
|
|
3297
4015
|
}
|
|
3298
|
-
|
|
3299
|
-
const risk = computeRepositoryRiskSummary({
|
|
4016
|
+
return {
|
|
3300
4017
|
structural,
|
|
3301
4018
|
evolution,
|
|
3302
4019
|
external
|
|
3303
|
-
}
|
|
4020
|
+
};
|
|
4021
|
+
};
|
|
4022
|
+
var runAnalyzeCommand = async (inputPath, authorIdentityMode, logger = createSilentLogger()) => {
|
|
4023
|
+
const analysisInputs = await collectAnalysisInputs(inputPath, authorIdentityMode, logger);
|
|
4024
|
+
logger.info("computing risk summary");
|
|
4025
|
+
const risk = computeRepositoryRiskSummary(analysisInputs);
|
|
3304
4026
|
logger.info(`analysis completed (repositoryScore=${risk.repositoryScore})`);
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
evolution,
|
|
3308
|
-
external,
|
|
4027
|
+
return {
|
|
4028
|
+
...analysisInputs,
|
|
3309
4029
|
risk
|
|
3310
4030
|
};
|
|
3311
|
-
|
|
4031
|
+
};
|
|
4032
|
+
|
|
4033
|
+
// src/application/run-explain-command.ts
|
|
4034
|
+
var selectTargets = (trace, summary, options) => {
|
|
4035
|
+
if (options.file !== void 0) {
|
|
4036
|
+
const normalized = options.file.replaceAll("\\", "/");
|
|
4037
|
+
return trace.targets.filter(
|
|
4038
|
+
(target) => target.targetType === "file" && target.targetId === normalized
|
|
4039
|
+
);
|
|
4040
|
+
}
|
|
4041
|
+
if (options.module !== void 0) {
|
|
4042
|
+
return trace.targets.filter(
|
|
4043
|
+
(target) => target.targetType === "module" && target.targetId === options.module
|
|
4044
|
+
);
|
|
4045
|
+
}
|
|
4046
|
+
const top = Math.max(1, options.top);
|
|
4047
|
+
const topFiles = summary.risk.hotspots.slice(0, top).map((entry) => entry.file);
|
|
4048
|
+
const fileSet = new Set(topFiles);
|
|
4049
|
+
return trace.targets.filter(
|
|
4050
|
+
(target) => target.targetType === "repository" || target.targetType === "file" && fileSet.has(target.targetId)
|
|
4051
|
+
);
|
|
4052
|
+
};
|
|
4053
|
+
var runExplainCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
|
|
4054
|
+
const analysisInputs = await collectAnalysisInputs(inputPath, authorIdentityMode, logger);
|
|
4055
|
+
logger.info("computing explainable risk summary");
|
|
4056
|
+
const evaluation = evaluateRepositoryRisk(analysisInputs, { explain: true });
|
|
4057
|
+
if (evaluation.trace === void 0) {
|
|
4058
|
+
throw new Error("risk trace unavailable");
|
|
4059
|
+
}
|
|
4060
|
+
const summary = {
|
|
4061
|
+
...analysisInputs,
|
|
4062
|
+
risk: evaluation.summary
|
|
4063
|
+
};
|
|
4064
|
+
logger.info(`explanation completed (repositoryScore=${summary.risk.repositoryScore})`);
|
|
4065
|
+
return {
|
|
4066
|
+
summary,
|
|
4067
|
+
trace: evaluation.trace,
|
|
4068
|
+
selectedTargets: selectTargets(evaluation.trace, summary, options)
|
|
4069
|
+
};
|
|
3312
4070
|
};
|
|
3313
4071
|
|
|
3314
4072
|
// src/index.ts
|
|
@@ -3340,6 +4098,38 @@ program.command("analyze").argument("[path]", "path to the project to analyze").
|
|
|
3340
4098
|
`);
|
|
3341
4099
|
}
|
|
3342
4100
|
);
|
|
4101
|
+
program.command("explain").argument("[path]", "path to the project to analyze").addOption(
|
|
4102
|
+
new Option(
|
|
4103
|
+
"--author-identity <mode>",
|
|
4104
|
+
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
4105
|
+
).choices(["likely_merge", "strict_email"]).default("likely_merge")
|
|
4106
|
+
).addOption(
|
|
4107
|
+
new Option(
|
|
4108
|
+
"--log-level <level>",
|
|
4109
|
+
"log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
|
|
4110
|
+
).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
|
|
4111
|
+
).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").addOption(
|
|
4112
|
+
new Option("--format <mode>", "output format: text, json, md").choices(["text", "json", "md"]).default("text")
|
|
4113
|
+
).action(
|
|
4114
|
+
async (path, options) => {
|
|
4115
|
+
const logger = createStderrLogger(options.logLevel);
|
|
4116
|
+
const top = Number.parseInt(options.top, 10);
|
|
4117
|
+
const explainOptions = {
|
|
4118
|
+
...options.file === void 0 ? {} : { file: options.file },
|
|
4119
|
+
...options.module === void 0 ? {} : { module: options.module },
|
|
4120
|
+
top: Number.isFinite(top) ? top : 5,
|
|
4121
|
+
format: options.format
|
|
4122
|
+
};
|
|
4123
|
+
const result = await runExplainCommand(
|
|
4124
|
+
path,
|
|
4125
|
+
options.authorIdentity,
|
|
4126
|
+
explainOptions,
|
|
4127
|
+
logger
|
|
4128
|
+
);
|
|
4129
|
+
process.stdout.write(`${formatExplainOutput(result, options.format)}
|
|
4130
|
+
`);
|
|
4131
|
+
}
|
|
4132
|
+
);
|
|
3343
4133
|
program.command("dependency-risk").argument("<dependency>", "dependency spec to evaluate (for example: react or react@19.0.0)").addOption(
|
|
3344
4134
|
new Option(
|
|
3345
4135
|
"--log-level <level>",
|