@getcodesentinel/codesentinel 1.6.4 → 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/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
- metrics: result.external.metrics,
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
- const frequencyRisk = normalizeWithScale(
2970
- logScale(evolutionMetrics.commitCount),
2971
- evolutionScales.commitCount
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 baseline = structuralFactor * dimensionWeights.structural + evolutionFactor * dimensionWeights.evolution + externalFactor * dimensionWeights.external;
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
- structuralFactor * evolutionFactor * config.interactionWeights.structuralEvolution,
2990
- structuralCentrality * evolutionFactor * config.interactionWeights.centralInstability,
2991
- externalFactor * Math.max(structuralFactor, evolutionFactor) * config.interactionWeights.dependencyAmplification
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: round44(repositoryNormalizedScore * 100),
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
- return computeRiskSummary(input.structural, input.evolution, input.external, config);
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 runAnalyzeCommand = async (inputPath, authorIdentityMode, logger = createSilentLogger()) => {
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
- repositoryPath: targetPath,
3277
- config: { authorIdentityMode }
3278
- }, createEvolutionProgressReporter(logger));
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
- logger.info("computing risk summary");
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
- const summary = {
3306
- structural,
3307
- evolution,
3308
- external,
4027
+ return {
4028
+ ...analysisInputs,
3309
4029
  risk
3310
4030
  };
3311
- return summary;
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>",