#
tokens: 23303/50000 4/60 files (page 2/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 2. Use http://codebase.md/arvindand/maven-tools-mcp?page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .gitattributes
├── .github
│   └── workflows
│       ├── ci.yml
│       └── docker.yml
├── .gitignore
├── .mvn
│   └── wrapper
│       └── maven-wrapper.properties
├── assets
│   └── demo.gif
├── build
│   ├── build-docker.cmd
│   ├── build-docker.sh
│   ├── build.cmd
│   ├── build.sh
│   └── README.md
├── CHANGELOG.md
├── CORPORATE-CERTIFICATES.md
├── docker-compose.yml
├── LICENSE
├── mvnw
├── mvnw.cmd
├── pom.xml
├── README.md
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── arvindand
    │   │           └── mcp
    │   │               └── maven
    │   │                   ├── config
    │   │                   │   ├── CacheConfig.java
    │   │                   │   ├── CacheConstants.java
    │   │                   │   ├── Context7Properties.java
    │   │                   │   ├── HttpClientConfig.java
    │   │                   │   ├── JacksonConfig.java
    │   │                   │   ├── MavenCentralProperties.java
    │   │                   │   ├── McpToolsConfig.java
    │   │                   │   └── NativeImageConfiguration.java
    │   │                   ├── MavenMcpServerApplication.java
    │   │                   ├── model
    │   │                   │   ├── BulkCheckResult.java
    │   │                   │   ├── Context7Guidance.java
    │   │                   │   ├── DependencyAge.java
    │   │                   │   ├── DependencyAgeAnalysis.java
    │   │                   │   ├── DependencyInfo.java
    │   │                   │   ├── MavenArtifact.java
    │   │                   │   ├── MavenCoordinate.java
    │   │                   │   ├── MavenMetadata.java
    │   │                   │   ├── McpError.java
    │   │                   │   ├── ProjectHealthAnalysis.java
    │   │                   │   ├── ReleasePatternAnalysis.java
    │   │                   │   ├── StabilityFilter.java
    │   │                   │   ├── ToolResponse.java
    │   │                   │   ├── VersionComparison.java
    │   │                   │   ├── VersionInfo.java
    │   │                   │   ├── VersionsByType.java
    │   │                   │   └── VersionTimelineAnalysis.java
    │   │                   ├── service
    │   │                   │   ├── MavenCentralException.java
    │   │                   │   ├── MavenCentralService.java
    │   │                   │   └── MavenDependencyTools.java
    │   │                   └── util
    │   │                       ├── MavenCoordinateParser.java
    │   │                       └── VersionComparator.java
    │   └── resources
    │       ├── application-docker.yaml
    │       ├── application-no-context7.yaml
    │       ├── application.yaml
    │       ├── logback-spring.xml
    │       └── META-INF
    │           └── additional-spring-configuration-metadata.json
    └── test
        ├── java
        │   └── com
        │       └── arvindand
        │           └── mcp
        │               └── maven
        │                   ├── config
        │                   │   └── Context7PropertiesTest.java
        │                   ├── MavenMcpServerIT.java
        │                   ├── model
        │                   │   └── Context7GuidanceTest.java
        │                   ├── service
        │                   │   ├── MavenCentralServiceRepositoryIT.java
        │                   │   ├── MavenCentralServiceUnitTest.java
        │                   │   ├── MavenDependencyToolsContext7EnabledIT.java
        │                   │   ├── MavenDependencyToolsIT.java
        │                   │   └── MavenDependencyToolsPerformanceIT.java
        │                   ├── TestHelpers.java
        │                   └── util
        │                       ├── MavenCoordinateParserTest.java
        │                       └── VersionComparatorTest.java
        └── resources
            └── application-test.yaml
```

# Files

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/util/VersionComparator.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.util;

import com.arvindand.mcp.maven.model.VersionInfo.VersionType;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.springframework.stereotype.Component;

/**
 * Utility for comparing and analyzing Maven version strings.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
@Component
public final class VersionComparator implements Comparator<String> {

  private static final String UNKNOWN = "unknown";
  private static final String ALPHA = "alpha";
  private static final String BETA = "beta";
  private static final String MILESTONE = "milestone";
  private static final String PATCH = "patch";
  private static final Set<String> STABLE_QUALIFIERS = Set.of("final", "ga", "release");
  private static final Set<String> ALPHA_QUALIFIERS = Set.of(ALPHA, "a");
  private static final Set<String> BETA_QUALIFIERS = Set.of(BETA, "b");
  private static final Set<String> MILESTONE_QUALIFIERS = Set.of(MILESTONE, "m");
  private static final Set<String> RC_QUALIFIERS = Set.of("rc", "cr");

  /**
   * Compares two version strings using Maven's ComparableVersion.
   *
   * @param version1 first version to compare
   * @param version2 second version to compare
   * @return negative if version1 < version2, 0 if equal, positive if version1 > version2
   */
  @Override
  public int compare(String version1, String version2) {
    return switch ((version1 == null ? 1 : 0) + (version2 == null ? 2 : 0)) {
      case 0 ->
          new ComparableVersion(version1)
              .compareTo(new ComparableVersion(version2)); // both non-null
      case 1 -> -1; // version1 is null, version2 is not
      case 2 -> 1; // version2 is null, version1 is not
      case 3 -> 0; // both null
      default -> throw new IllegalStateException("Unexpected comparison state");
    };
  }

  /**
   * Gets the latest version from an array of version strings.
   *
   * @param versions array of version strings
   * @return the latest version or null if array is empty
   */
  public static String getLatest(String[] versions) {
    if (versions == null || versions.length == 0) return null;
    return Arrays.stream(versions).max(new VersionComparator()).orElse(null);
  }

  /**
   * Determines the type of update between current and latest versions.
   *
   * @param currentVersion the current version
   * @param latestVersion the latest available version
   * @return update type: "major", "minor", "patch", "none", or "unknown"
   */
  public String determineUpdateType(String currentVersion, String latestVersion) {
    return switch (validateVersions(currentVersion, latestVersion)) {
      case INVALID -> UNKNOWN;
      case EQUAL -> "none";
      case VALID -> {
        int comparison = compare(currentVersion, latestVersion);
        if (comparison >= 0) {
          yield comparison == 0 ? "none" : UNKNOWN;
        } else {
          yield determineUpdateType(parseVersion(currentVersion), parseVersion(latestVersion));
        }
      }
    };
  }

  /**
   * Checks if a version is considered stable (not pre-release).
   *
   * @param version the version to check
   * @return true if the version is stable
   */
  public boolean isStableVersion(String version) {
    if (version == null) return false;

    return switch (classifyQualifier(extractQualifier(version))) {
      case STABLE -> true;
      case PRE_RELEASE -> false;
    };
  }

  /**
   * Gets the version type enum for a version string.
   *
   * @param version the version to analyze
   * @return the version type enum
   */
  public VersionType getVersionType(String version) {
    if (version == null) return VersionType.STABLE;

    return switch (classifyQualifier(extractQualifier(version))) {
      case STABLE -> VersionType.STABLE;
      case PRE_RELEASE -> determinePreReleaseVersionType(extractQualifier(version));
    };
  }

  /**
   * Gets the version type as a display string.
   *
   * @param version the version to analyze
   * @return the version type as a string
   */
  public String getVersionTypeString(String version) {
    if (version == null) return UNKNOWN;
    return getVersionType(version).getDisplayName();
  }

  /**
   * Parses a version string into numeric components and qualifier.
   *
   * @param version the version string to parse
   * @return parsed version components
   */
  public VersionComponents parseVersion(String version) {
    if (version == null || version.trim().isEmpty()) {
      return new VersionComponents(new int[0], "");
    }

    String trimmed = version.trim();

    // Split on first hyphen to separate numeric part from qualifier
    int hyphenIndex = findFirstQualifierSeparator(trimmed);
    String numericPart = hyphenIndex != -1 ? trimmed.substring(0, hyphenIndex) : trimmed;
    String qualifier = hyphenIndex != -1 ? trimmed.substring(hyphenIndex + 1).toLowerCase() : "";

    // Parse numeric components (major.minor.patch.etc)
    String[] segments = numericPart.split("\\.");
    int[] numericParts = new int[segments.length];

    for (int i = 0; i < segments.length; i++) {
      try {
        // Handle cases like "1.0.0-SNAPSHOT" where hyphen is within a segment
        String segment = segments[i];
        int segmentHyphen = segment.indexOf('-');
        if (segmentHyphen != -1) {
          segment = segment.substring(0, segmentHyphen);
          // If this is the first time we see a qualifier, capture it
          if (qualifier.isEmpty()) {
            qualifier = segments[i].substring(segmentHyphen + 1).toLowerCase();
          }
        }
        numericParts[i] = Integer.parseInt(segment);
      } catch (NumberFormatException _) {
        numericParts[i] = 0;
      }
    }

    return new VersionComponents(numericParts, qualifier);
  }

  private int findFirstQualifierSeparator(String version) {
    // Look for common qualifier separators: - or _
    // But be smart about it - avoid separating dates (2023-01-15) or similar patterns
    int hyphenIndex = version.indexOf('-');
    int underscoreIndex = version.indexOf('_');

    // Return the first separator found, preferring hyphen
    if (hyphenIndex == -1 && underscoreIndex == -1) return -1;
    if (hyphenIndex == -1) return underscoreIndex;
    if (underscoreIndex == -1) return hyphenIndex;
    return Math.min(hyphenIndex, underscoreIndex);
  }

  private String extractQualifier(String version) {
    return parseVersion(version).qualifier();
  }

  private ValidationResult validateVersions(String current, String latest) {
    if (current == null || latest == null) return ValidationResult.INVALID;
    if (current.equals(latest)) return ValidationResult.EQUAL;
    return ValidationResult.VALID;
  }

  private QualifierType classifyQualifier(String qualifier) {
    if (qualifier.isEmpty() || STABLE_QUALIFIERS.contains(qualifier)) {
      return QualifierType.STABLE;
    }

    String lower = qualifier.toLowerCase();

    // Treat service packs as stable (e.g., 1.0.0-SP1)
    if (lower.startsWith("sp")) {
      return QualifierType.STABLE;
    }

    // Common pre-release markers
    if (lower.contains("snapshot")
        || lower.contains("rc")
        || lower.contains("cr")
        || lower.contains("m")
        || lower.contains(BETA)
        || lower.contains(ALPHA)
        || lower.contains("preview")
        || lower.contains("dev")) {
      return QualifierType.PRE_RELEASE;
    }

    // Unknown qualifier: conservatively treat as pre-release rather than stable
    return QualifierType.PRE_RELEASE;
  }

  private VersionType determinePreReleaseVersionType(String qualifier) {
    if (isAlphaQualifier(qualifier)) return VersionType.ALPHA;
    if (isBetaQualifier(qualifier)) return VersionType.BETA;
    if (isMilestoneQualifier(qualifier)) return VersionType.MILESTONE;
    if (isRcQualifier(qualifier)) return VersionType.RC;
    return VersionType.ALPHA; // Default unknown pre-releases to alpha
  }

  private boolean isAlphaQualifier(String qualifier) {
    return ALPHA_QUALIFIERS.contains(qualifier)
        || qualifier.startsWith(ALPHA)
        || qualifier.startsWith("a")
        || qualifier.contains("dev")
        || qualifier.contains("preview");
  }

  private boolean isBetaQualifier(String qualifier) {
    return BETA_QUALIFIERS.contains(qualifier)
        || qualifier.startsWith(BETA)
        || qualifier.startsWith("b");
  }

  private boolean isMilestoneQualifier(String qualifier) {
    return MILESTONE_QUALIFIERS.contains(qualifier)
        || qualifier.startsWith(MILESTONE)
        || qualifier.startsWith("m");
  }

  private boolean isRcQualifier(String qualifier) {
    return RC_QUALIFIERS.contains(qualifier)
        || qualifier.startsWith("rc")
        || qualifier.startsWith("cr")
        || qualifier.contains("candidate");
  }

  private String determineUpdateType(VersionComponents current, VersionComponents latest) {
    // Handle case where numeric parts are identical but qualifiers differ
    boolean sameNumericVersion = areNumericVersionsEqual(current, latest);

    if (sameNumericVersion) {
      // If numeric versions are the same, check qualifiers
      return determineQualifierUpdate(current.qualifier(), latest.qualifier());
    }

    // Compare numeric parts to determine update type
    int maxLength = Math.max(current.numericParts().length, latest.numericParts().length);

    for (int i = 0; i < maxLength; i++) {
      int currentPart = i < current.numericParts().length ? current.numericParts()[i] : 0;
      int latestPart = i < latest.numericParts().length ? latest.numericParts()[i] : 0;

      if (latestPart > currentPart) {
        return switch (i) {
          case 0 -> "major";
          case 1 -> "minor";
          default -> PATCH;
        };
      } else if (currentPart > latestPart) {
        // Current version is higher than "latest" - this is a downgrade scenario
        return UNKNOWN;
      }
    }
    return "none";
  }

  private boolean areNumericVersionsEqual(VersionComponents current, VersionComponents latest) {
    int maxLength = Math.max(current.numericParts().length, latest.numericParts().length);

    for (int i = 0; i < maxLength; i++) {
      int currentPart = i < current.numericParts().length ? current.numericParts()[i] : 0;
      int latestPart = i < latest.numericParts().length ? latest.numericParts()[i] : 0;

      if (currentPart != latestPart) {
        return false;
      }
    }
    return true;
  }

  private String determineQualifierUpdate(String currentQualifier, String latestQualifier) {
    // If both are empty, versions are identical
    if (currentQualifier.isEmpty() && latestQualifier.isEmpty()) {
      return "none";
    }

    // If current has qualifier but latest doesn't, it's upgrading to stable
    if (!currentQualifier.isEmpty() && latestQualifier.isEmpty()) {
      return PATCH; // Treat pre-release to stable as patch update
    }

    // If current is stable but latest has qualifier, this is unusual (downgrade to pre-release)
    if (currentQualifier.isEmpty() && !latestQualifier.isEmpty()) {
      return UNKNOWN;
    }

    // Both have qualifiers - compare stability levels
    return compareQualifierStability(currentQualifier, latestQualifier);
  }

  private String compareQualifierStability(String currentQualifier, String latestQualifier) {
    // Define stability order (lower index = less stable)
    String[] stabilityOrder = {ALPHA, BETA, MILESTONE, "rc", "snapshot"};

    int currentStability = getQualifierStability(currentQualifier, stabilityOrder);
    int latestStability = getQualifierStability(latestQualifier, stabilityOrder);

    if (latestStability > currentStability) {
      return PATCH; // Upgrading to more stable pre-release
    } else if (latestStability < currentStability) {
      return UNKNOWN; // Downgrading to less stable
    } else {
      // Same stability level - might be version number change within qualifier
      return currentQualifier.equals(latestQualifier) ? "none" : PATCH;
    }
  }

  private int getQualifierStability(String qualifier, String[] stabilityOrder) {
    String lowerQualifier = qualifier.toLowerCase();

    for (int i = 0; i < stabilityOrder.length; i++) {
      if (lowerQualifier.contains(stabilityOrder[i])) {
        return i;
      }
    }

    // Unknown qualifier types get medium stability
    return stabilityOrder.length / 2;
  }

  /**
   * Represents parsed version components.
   *
   * @param numericParts the numeric parts of the version
   * @param qualifier the qualifier part (e.g., "alpha", "beta")
   */
  public record VersionComponents(int[] numericParts, String qualifier) {
    @Override
    public boolean equals(Object obj) {
      if (this == obj) return true;
      if (obj == null || getClass() != obj.getClass()) return false;
      VersionComponents that = (VersionComponents) obj;
      return Arrays.equals(numericParts, that.numericParts)
          && Objects.equals(qualifier, that.qualifier);
    }

    @Override
    public int hashCode() {
      return Objects.hash(Arrays.hashCode(numericParts), qualifier);
    }

    @Override
    public String toString() {
      return "VersionComponents{numericParts="
          + Arrays.toString(numericParts)
          + ", qualifier='"
          + qualifier
          + "'}";
    }
  }

  private enum ValidationResult {
    INVALID,
    EQUAL,
    VALID
  }

  private enum QualifierType {
    STABLE,
    PRE_RELEASE
  }
}

```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/util/VersionComparatorTest.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.util;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

/**
 * Comprehensive unit tests for VersionComparator.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
class VersionComparatorTest {

  private VersionComparator versionComparator;

  @BeforeEach
  void setUp() {
    versionComparator = new VersionComparator();
  }

  /** Test data for version comparison. */
  private static Stream<Arguments> versionComparisonTestData() {
    return Stream.of(
        // Basic numeric comparisons
        Arguments.of("1.0.0", "2.0.0", -1, "1.0.0 < 2.0.0"),
        Arguments.of("2.0.0", "1.0.0", 1, "2.0.0 > 1.0.0"),
        Arguments.of("1.0.0", "1.0.0", 0, "1.0.0 = 1.0.0"),

        // Minor version comparisons
        Arguments.of("1.0.0", "1.1.0", -1, "1.0.0 < 1.1.0"),
        Arguments.of("1.5.0", "1.2.0", 1, "1.5.0 > 1.2.0"),

        // Patch version comparisons
        Arguments.of("1.0.0", "1.0.1", -1, "1.0.0 < 1.0.1"),
        Arguments.of("1.0.5", "1.0.2", 1, "1.0.5 > 1.0.2"),

        // Different length versions - should be treated as equal
        Arguments.of("1.0", "1.0.0", 0, "1.0 = 1.0.0 (equivalent versions)"),
        Arguments.of("1.0.0", "1.0", 0, "1.0.0 = 1.0 (equivalent versions)"),
        Arguments.of("1", "1.0.0", 0, "1 = 1.0.0 (equivalent versions)"),
        Arguments.of("1.0", "1.0.1", -1, "1.0 < 1.0.1"),
        Arguments.of("1.1", "1.0.1", 1, "1.1 > 1.0.1"),

        // Large numbers
        Arguments.of("10.0.0", "9.9.9", 1, "10.0.0 > 9.9.9"),
        Arguments.of("1.10.0", "1.9.0", 1, "1.10.0 > 1.9.0"),
        Arguments.of("1.0.10", "1.0.9", 1, "1.0.10 > 1.0.9"),

        // Multi-digit versions
        Arguments.of("1.2.3", "1.2.10", -1, "1.2.3 < 1.2.10"),
        Arguments.of(
            "11.0.0",
            "2.0.0",
            1,
            "11.0.0 > 2.0.0"), // Qualifier comparisons (release > rc > milestone > beta > alpha)
        Arguments.of("1.0.0", "1.0.0-RC1", 1, "release > rc"),
        Arguments.of("1.0.0-RC1", "1.0.0-M1", 1, "rc > milestone"),
        Arguments.of("1.0.0-M1", "1.0.0-BETA1", 1, "milestone > beta"),
        Arguments.of("1.0.0-BETA1", "1.0.0-ALPHA1", 1, "beta > alpha"),

        // Same qualifier with different numbers
        Arguments.of("1.0.0-RC1", "1.0.0-RC2", -1, "RC1 < RC2"),
        Arguments.of("1.0.0-BETA2", "1.0.0-BETA1", 1, "BETA2 > BETA1"),
        Arguments.of("1.0.0-M2", "1.0.0-M1", 1, "M2 > M1"), // Different separators
        Arguments.of("1.0.0", "1-0-0", 0, "Different separators treated equally"),
        Arguments.of("1.0.0", "1_0_0", -1, "Underscore separators (Maven treats differently)"),
        Arguments.of(
            "1.0.0-RC1",
            "1.0.0.RC1",
            0,
            "Different qualifier separators (Maven treats as equivalent)"),

        // Case insensitive qualifiers
        Arguments.of("1.0.0-rc1", "1.0.0-RC1", 0, "Case insensitive RC"),
        Arguments.of("1.0.0-beta", "1.0.0-BETA", 0, "Case insensitive BETA"),
        Arguments.of("1.0.0-alpha", "1.0.0-ALPHA", 0, "Case insensitive ALPHA"),

        // Special qualifiers - Maven's official behavior
        Arguments.of("1.0.0.RELEASE", "1.0.0", 0, "RELEASE qualifier equivalent to plain"),
        Arguments.of("1.0.0.Final", "1.0.0", 0, "Final qualifier equivalent to plain"),
        Arguments.of("1.0.0.GA", "1.0.0", 0, "GA qualifier equivalent to plain"),

        // Complex real-world examples
        Arguments.of("3.2.0", "3.2.0-RC1", 1, "Spring Boot style"),
        Arguments.of("2.15.2", "2.16.0-SNAPSHOT", -1, "Jackson style with snapshot"),
        Arguments.of("6.1.4", "6.1.4.RELEASE", 0, "Spring Framework style"));
  }

  @ParameterizedTest(name = "{3}")
  @MethodSource("versionComparisonTestData")
  void testCompare(String version1, String version2, int expectedSign, String description) {
    // When
    int result = versionComparator.compare(version1, version2);

    // Then - Check the sign of the result
    if (expectedSign == 0) {
      assertThat(result).isZero();
    } else if (expectedSign < 0) {
      assertThat(result).isNegative();
    } else {
      assertThat(result).isPositive();
    }
  }

  @Test
  void testCompare_NullAndEmptyVersions() {
    // Null versions should be handled gracefully or throw appropriate exceptions
    // The exact behavior depends on implementation, but it should be consistent

    try {
      int result1 = versionComparator.compare(null, "1.0.0");
      int result2 = versionComparator.compare("1.0.0", null);
      int result3 = versionComparator.compare(null, null);

      // If no exception, results should be consistent
      assertThat(result1).isNotEqualTo(result2); // Should be opposites
      assertThat(result3).isZero(); // null equals null
    } catch (NullPointerException e) {
      // This is also acceptable behavior
      assertThat(e).isInstanceOf(NullPointerException.class);
    }
  }

  @Test
  void testCompare_SameVersionsAreEqual() {
    String[] versions = {"1.0.0", "2.5.1", "1.0.0-RC1", "1.0.0-BETA", "1.0.0-ALPHA", "1.0.0-M1"};

    for (String version : versions) {
      int result = versionComparator.compare(version, version);
      assertThat(result).isZero();
    }
  }

  @Test
  void testGetLatest_BasicVersions() {
    // Given
    String[] versions = {"1.0.0", "2.0.0", "1.5.0", "2.1.0", "1.0.1"};

    // When
    String latest = VersionComparator.getLatest(versions);

    // Then
    assertThat(latest).isEqualTo("2.1.0");
  }

  @Test
  void testGetLatest_WithQualifiers() {
    // Given
    String[] versions = {"1.0.0", "1.0.0-RC1", "1.0.0-BETA", "1.0.0-ALPHA", "1.0.0-M1"};

    // When
    String latest = VersionComparator.getLatest(versions);

    // Then - Release version should be latest
    assertThat(latest).isEqualTo("1.0.0");
  }

  @Test
  void testGetLatest_RealWorldExample() {
    // Given - Realistic Spring Boot versions
    String[] versions = {
      "3.1.0", "3.1.1", "3.1.2", "3.2.0-RC1", "3.2.0-SNAPSHOT", "3.2.0", "3.1.5", "3.0.12"
    };

    // When
    String latest = VersionComparator.getLatest(versions);

    // Then - Should be the highest stable release
    assertThat(latest).isEqualTo("3.2.0");
  }

  @Test
  void testGetLatest_EmptyArray() {
    // Given
    String[] versions = {};

    // When
    String latest = VersionComparator.getLatest(versions);

    // Then
    assertThat(latest).isNull();
  }

  @Test
  void testGetLatest_NullArray() {
    // Given
    String[] versions = null;

    // When
    String latest = VersionComparator.getLatest(versions);

    // Then
    assertThat(latest).isNull();
  }

  @Test
  void testGetLatest_SingleVersion() {
    // Given
    String[] versions = {"1.0.0"};

    // When
    String latest = VersionComparator.getLatest(versions);

    // Then
    assertThat(latest).isEqualTo("1.0.0");
  }

  @Test
  void testSorting_DescendingOrder() {
    // Given
    List<String> versions = Arrays.asList("1.0.0", "3.0.0", "2.0.0", "1.5.0", "2.1.0");

    // When - Sort in descending order
    List<String> sorted = versions.stream().sorted(versionComparator.reversed()).toList();

    // Then
    assertThat(sorted).containsExactly("3.0.0", "2.1.0", "2.0.0", "1.5.0", "1.0.0");
  }

  @Test
  void testSorting_AscendingOrder() {
    // Given
    List<String> versions = Arrays.asList("3.0.0", "1.0.0", "2.1.0", "1.5.0", "2.0.0");

    // When - Sort in ascending order
    List<String> sorted = versions.stream().sorted(versionComparator).toList();

    // Then
    assertThat(sorted).containsExactly("1.0.0", "1.5.0", "2.0.0", "2.1.0", "3.0.0");
  }

  @Test
  void testQualifierPriority() {
    // Given - Same base version with different qualifiers
    List<String> versions =
        Arrays.asList("1.0.0-M1", "1.0.0-ALPHA", "1.0.0-BETA", "1.0.0-RC1", "1.0.0");

    // When - Sort in ascending order
    List<String> sorted =
        versions.stream()
            .sorted(versionComparator)
            .toList(); // Then - Should be in Maven order: alpha < beta < milestone < rc < release
    assertThat(sorted)
        .containsExactly("1.0.0-ALPHA", "1.0.0-BETA", "1.0.0-M1", "1.0.0-RC1", "1.0.0");
  }

  @Test
  void testMixedVersionFormats() {
    // Given - Versions with different formats
    List<String> versions = Arrays.asList("1", "1.0", "1.0.0", "1.0.1", "1.1", "2");

    // When - Sort in ascending order
    List<String> sorted = versions.stream().sorted(versionComparator).toList();

    // Then - Should handle different formats correctly
    assertThat(sorted).containsExactly("1", "1.0", "1.0.0", "1.0.1", "1.1", "2");
  }

  @Test
  void testConsistencyWithEquals() {
    // Given - Versions that should be equal
    String[][] equalVersionPairs = {
      {"1.0.0", "1.0.0"},
      {"1.0", "1.0.0"}, // Assuming this should be equal
      {"1.0.0-RC1", "1.0.0-rc1"}
    };

    for (String[] pair : equalVersionPairs) {
      // When
      int result1 = versionComparator.compare(pair[0], pair[1]);
      int result2 = versionComparator.compare(pair[1], pair[0]);

      // Then - If equal, both comparisons should return 0
      if (result1 == 0) {
        assertThat(result2).isZero();
      } else {
        // If not equal, they should be opposites
        assertThat(result1 * result2).isNegative();
      }
    }
  }

  @Test
  void testTransitivity() {
    // Given - Three versions
    String version1 = "1.0.0";
    String version2 = "1.5.0";
    String version3 = "2.0.0";

    // When
    int compare12 = versionComparator.compare(version1, version2);
    int compare23 = versionComparator.compare(version2, version3);
    int compare13 = versionComparator.compare(version1, version3);

    // Then - If version1 < version2 and version2 < version3, then version1 < version3
    if (compare12 < 0 && compare23 < 0) {
      assertThat(compare13).isNegative();
    }
  }

  @Test
  void testLargeVersionNumbers() {
    // Given - Versions with large numbers
    String[] versions = {"999.999.999", "1000.0.0", "1000.1.0"};

    // When
    String latest = VersionComparator.getLatest(versions);

    // Then
    assertThat(latest).isEqualTo("1000.1.0");
  }

  @Test
  void testVersionsWithLeadingZeros() {
    // Given - Versions that might have leading zeros
    String version1 = "1.01.0";
    String version2 = "1.1.0";

    // When
    int result = versionComparator.compare(version1, version2);

    // Then - Should handle leading zeros correctly (01 should equal 1)
    assertThat(result).isZero();
  }

  @ParameterizedTest
  @MethodSource("updateTypeTestData")
  void testDetermineUpdateType(
      String current, String latest, String expectedUpdateType, String description) {
    // When
    String updateType = versionComparator.determineUpdateType(current, latest);

    // Then
    assertThat(updateType).as(description).isEqualTo(expectedUpdateType);
  }

  @Test
  void testCriticalVersionParsingScenarios() {
    // Test the critical parsing scenarios that were failing before the fix

    // Spring Boot milestone versions (previously corrupted by ComparableVersion.toString())
    assertThat(versionComparator.determineUpdateType("2.0.0-M1", "2.0.0")).isEqualTo("patch");
    assertThat(versionComparator.determineUpdateType("1.5.0", "2.0.0-M1")).isEqualTo("major");

    // Complex pre-release versions
    assertThat(versionComparator.determineUpdateType("3.1.0-RC1", "3.1.0")).isEqualTo("patch");
    assertThat(versionComparator.determineUpdateType("1.0.0-alpha", "1.0.0-beta"))
        .isEqualTo("patch");

    // Verify version parsing doesn't corrupt the original strings
    VersionComparator.VersionComponents parsed1 = versionComparator.parseVersion("2.0.0-M1");
    assertThat(parsed1.qualifier()).isEqualTo("m1");
    assertThat(parsed1.numericParts()).containsExactly(2, 0, 0);

    VersionComparator.VersionComponents parsed2 = versionComparator.parseVersion("1.0.0-SNAPSHOT");
    assertThat(parsed2.qualifier()).isEqualTo("snapshot");
    assertThat(parsed2.numericParts()).containsExactly(1, 0, 0);
  }

  private static Stream<Arguments> updateTypeTestData() {
    return Stream.of(
        // Major updates
        Arguments.of("1.0.0", "2.0.0", "major", "Major version update"),
        Arguments.of("1.5.3", "2.0.0", "major", "Major version update with minor/patch"),
        Arguments.of("1.0.0-alpha", "2.0.0", "major", "Major update from alpha"),

        // Minor updates
        Arguments.of("1.0.0", "1.1.0", "minor", "Minor version update"),
        Arguments.of("1.0.5", "1.2.0", "minor", "Minor update with patch difference"),
        Arguments.of("1.0.0-beta", "1.1.0", "minor", "Minor update from beta"),

        // Patch updates
        Arguments.of("1.0.0", "1.0.1", "patch", "Patch version update"),
        Arguments.of("1.2.3", "1.2.4", "patch", "Simple patch update"),
        Arguments.of("1.0.0-rc", "1.0.1", "patch", "Patch update from RC"),

        // No updates / equal versions
        Arguments.of("1.0.0", "1.0.0", "none", "Same version"),
        Arguments.of("1.0.0-alpha", "1.0.0-alpha", "none", "Same alpha version"),

        // Unknown/downgrade scenarios
        Arguments.of("2.0.0", "1.0.0", "unknown", "Downgrade scenario"),
        Arguments.of("1.1.0", "1.0.0", "unknown", "Minor downgrade"),
        Arguments.of(null, "1.0.0", "unknown", "Null current version"),
        Arguments.of("1.0.0", null, "unknown", "Null latest version"));
  }

  @ParameterizedTest
  @MethodSource("stableVersionTestData")
  void testIsStableVersion(String version, boolean expectedStable, String description) {
    // When
    boolean isStable = versionComparator.isStableVersion(version);

    // Then
    assertThat(isStable).as(description).isEqualTo(expectedStable);
  }

  private static Stream<Arguments> stableVersionTestData() {
    return Stream.of(
        // Stable versions
        Arguments.of("1.0.0", true, "Plain numeric version is stable"),
        Arguments.of("2.1.5", true, "Multi-component numeric version is stable"),
        Arguments.of("1.0.0-final", true, "Final qualifier is stable"),
        Arguments.of("1.0.0-ga", true, "GA qualifier is stable"),
        Arguments.of("1.0.0-release", true, "Release qualifier is stable"),
        Arguments.of("1.0.0-sp1", true, "Service pack is stable"),

        // Pre-release versions
        Arguments.of("1.0.0-alpha", false, "Alpha version is not stable"),
        Arguments.of("1.0.0-beta", false, "Beta version is not stable"),
        Arguments.of("1.0.0-rc", false, "RC version is not stable"),
        Arguments.of("1.0.0-snapshot", false, "Snapshot version is not stable"),
        Arguments.of("1.0.0-milestone", false, "Milestone version is not stable"),

        // Edge cases
        Arguments.of(null, false, "Null version is not stable"));
  }

  @ParameterizedTest
  @MethodSource("versionTypeTestData")
  void testGetVersionType(String version, String expectedType, String description) {
    // When
    String versionType = versionComparator.getVersionTypeString(version);

    // Then
    assertThat(versionType).as(description).isEqualTo(expectedType);
  }

  private static Stream<Arguments> versionTypeTestData() {
    return Stream.of(
        // Stable versions
        Arguments.of("1.0.0", "stable", "Plain numeric version is stable"),
        Arguments.of("1.0.0-final", "stable", "Final qualifier is stable"),
        Arguments.of("1.0.0-ga", "stable", "GA qualifier is stable"),
        Arguments.of("1.0.0-release", "stable", "Release qualifier is stable"),

        // Alpha versions
        Arguments.of("1.0.0-alpha", "alpha", "Alpha version"),
        Arguments.of("1.0.0-a1", "alpha", "Alpha with number"),
        Arguments.of("1.0.0-dev", "alpha", "Dev version treated as alpha"),
        Arguments.of("1.0.0-preview", "alpha", "Preview version treated as alpha"),

        // Beta versions
        Arguments.of("1.0.0-beta", "beta", "Beta version"),
        Arguments.of("1.0.0-b1", "beta", "Beta with number"),

        // RC versions
        Arguments.of("1.0.0-rc", "rc", "RC version"),
        Arguments.of("1.0.0-cr", "rc", "CR version treated as RC"),
        Arguments.of("1.0.0-candidate", "rc", "Candidate version treated as RC"),

        // Milestone versions
        Arguments.of("1.0.0-milestone", "milestone", "Milestone version"),
        Arguments.of("1.0.0-m1", "milestone", "Milestone with number"),

        // Edge cases
        Arguments.of(null, "unknown", "Null version returns unknown"),
        Arguments.of("1.0.0-custom", "alpha", "Unknown qualifier defaults to alpha for safety"));
  }
}

```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added (Unreleased)

### Changed (Unreleased)

### Fixed (Unreleased)

### Removed (Unreleased)

## [1.5.1] - 2025-10-22

**Corporate Environment Support Release** - Adds dual-image build strategy and comprehensive documentation for corporate networks with SSL inspection/MITM proxies.

### Added (1.5.1)

- **Corporate Certificate Guide**: Complete documentation (`CORPORATE-CERTIFICATES.md`) for building custom native images with corporate SSL certificates using Paketo buildpack certificate bindings
- **Dual-Image Build Strategy**: CI/CD now builds 4 image variants (amd64/arm64, with/without Context7) to support different deployment scenarios
- **Spring Profiles for Context7 Control**:
  - `application-no-context7.yaml` - Disables Context7 integration for corporate environments
  - `application-docker.yaml` - Controls Spring Boot banner for clean MCP protocol compliance
- **Image Variants Documentation**: Added comprehensive table in README explaining when to use each image tag (`latest`, `latest-noc7`, `<version>`, `<version>-noc7`)

### Changed (1.5.1)

- **Build Scripts**: Updated all Unix and Windows build scripts to support `-noc7` image builds with Spring profile activation
- **README**: Added troubleshooting section for corporate SSL environments and link to certificate guide
- **CI/CD Workflow**: Enhanced to build and publish 4 multi-architecture image manifests
- **Updated Dependencies**:
  - Resilience4j updated to 2.3.0 (from 2.2.0)

### Fixed (1.5.1)

- **SSL Handshake Issues**: Resolved Context7 connection failures in corporate environments with SSL inspection by providing `-noc7` image variants and custom certificate build solution
- **MCP Protocol Interference**: Fixed Spring Boot banner appearing before logback initialization by using `application-docker.yaml` profile

## [1.5.0] - 2025-10-19

**Performance & Resilience Release** - Introduces OkHttp 5 for HTTP/2 support, circuit breaker patterns, and improved reliability. Includes code quality improvements and dependency updates.

### Added (1.5.0)

- **OkHttp5 Integration**: Direct HTTP/2 support with connection pooling for improved performance and resource efficiency (5.2.1 - latest stable)
- **Resilience4j Patterns**: Added circuit breaker, retry, and rate limiter patterns to `MavenCentralService` for improved reliability
- **Connection Pool Configuration**: New `maven.central.connection-pool-size` property (default: 50) for tuning OkHttp connection pooling
- **Spring Configuration Metadata**: Added `maven.central.connection-pool-size` property documentation for IDE autocomplete support

### Changed (1.5.0)

- **HTTP Client Architecture**: Introduced OkHttp 5.2.1 as the primary HTTP client (replacing SimpleClientHttpRequestFactory) for HTTP/2 support and improved connection pooling
- **Updated Dependencies**:
  - Spring Boot parent updated to 3.5.6 (from 3.5.4)
  - fmt-maven-plugin updated to 2.29 (from 2.27)
- **MCP Client Transport**: Changed from SYNC SSE client to ASYNC streamable-http transport for better performance and compatibility

### Removed (1.5.0)

- **Test Utilities**: Removed unused `ClientStdio` test class (62 lines)
- **Legacy REST Configuration**: Removed direct `SimpleClientHttpRequestFactory` bean in favor of OkHttp5-backed RestClient

## [1.4.0] - 2025-08-17

**Direct Maven Repository Access Release** - All existing MCP tools and features are fully retained with significantly improved accuracy and performance by reading maven-metadata.xml files directly from Maven Central instead of using the search API.

### Added (1.4.0)

- **Maven Metadata XML Parsing:** Now reads maven-metadata.xml files directly from Maven Central repository for accurate version information
- **Jackson XML Support:** Added XML parsing capabilities for universal JVM build tool support
- **Direct Repository Tests:** Comprehensive test coverage for maven-metadata.xml access functionality
- **Improved Version Accuracy:** Eliminates search API delays and version ordering quirks

### Changed (1.4.0)

- **Data Source:** Now uses `https://repo1.maven.org/maven2` maven-metadata.xml files instead of `search.maven.org` Solr API
- **Timestamp Accuracy:** Implemented a more accurate timestamp retrieval method that fetches real timestamps for recent versions via HTTP HEAD requests.
- **Enhanced Performance:** Smaller XML metadata files provide faster response times than large JSON search results
- **Simplified Configuration:** Removed complex strategy patterns for cleaner, more maintainable codebase
- **Updated Dependencies:** Added Jackson XML module for maven-metadata.xml parsing support

### Fixed (1.4.0)

- **Version Ordering Issues:** Direct repository access provides accurate version ordering without Solr search index delays
- **Date-like Version Anomalies:** Fixed incorrect "latest" results for artifacts like commons-io with date-like versions
- **JGit Release Classification:** Service pack releases with `-r` suffix now correctly classified as stable
- **Timestamp Analysis:** Repository metadata provides authoritative version information for analytical features
- **Error Handling:** Graceful null returns for non-existent artifacts instead of exceptions for better API consistency
- **Date/Time Processing:** Improved timestamp parsing using proper Java time APIs for more accurate analytical features

### Removed (1.4.0)

- **Search API Dependency:** Eliminated reliance on `search.maven.org/solrsearch/select` for core functionality
- **Unused Models:** Removed the obsolete `MavenSearchResponse` model after refactoring.
- **Strategy Configuration:** Removed complex strategy patterns for simplified architecture  
- **Legacy Properties:** Deprecated `maven.central.base-url` in favor of `maven.central.repository-base-url`

## [1.3.0] - 2025-08-09

### Added (1.3.0)

- **Type-safe ToolResponse architecture**: Unified response wrapper for all MCP tools with sealed interface pattern
- **Performance optimizations**: Early-exit algorithms with 50-80% performance improvements
- **Enhanced test coverage**: Critical version parsing test scenarios for complex pre-release versions
- **Simplified Context7 orchestration**: Clear step-by-step tool usage instructions with web search fallback

### Changed (1.3.0)

- **BREAKING**: All MCP tool methods now return `ToolResponse` instead of JSON strings for better type safety
- **Context7Guidance model**: Simplified from 5 fields to single `orchestrationInstructions` field for better clarity
- **Library name resolution**: Now uses Maven artifactId directly instead of ecosystem-specific mapping for universal coverage
- **Response format**: Consistent response structure with `ToolResponse.Success<T>` and `ToolResponse.Error`
- **User-Agent Header**: Updated to Maven-Tools-MCP/1.3.0
- **Native image configuration**: Updated reflection hints to include ToolResponse and nested records
- **Algorithm optimizations**: Implemented early-exit optimization in version type classification
- **Stream operations**: Replaced manual loops with optimized stream operations where beneficial
- **Time calculations**: Deduplicated redundant timestamp arithmetic operations

### Fixed (1.3.0)

- **Critical version parsing bug**: Fixed corruption of pre-release versions (e.g., "2.0.0-M1" was becoming "2-milestone-1")
- **Cache configuration**: Resolved type collision issues by using separate cache instances per region
- **SonarQube warnings**: Resolved string literal duplication, cognitive complexity, and primitive null comparison issues

### Removed (1.3.0)

- **Unused code**: Removed duplicate ToolResponseOperation interface and associated error handling method
- **Obsolete dependencies**: Cleaned up JsonResponseService references and inline imports
- **Context7 complexity**: Eliminated redundant guidance fields (suggestedSearch, searchHints, complexity, documentationFocus)
- **Ecosystem-specific logic**: Removed 70+ lines of hardcoded Spring/Hibernate/Jackson library mapping for maintainability

## [1.2.0] - 2025-07-24

### Added (1.2.0)

- **Guided Delegation Architecture**: Context7 guidance hints in Maven tool responses for intelligent LLM orchestration
- `Context7Properties` configuration with `context7.enabled` setting (defaults to true)
- `Context7Guidance` model with smart search suggestions and ecosystem-specific hints
- Context7 guidance integration in response models (when context7.enabled=true):
  - `VersionComparisonResponse` includes migration guidance hints for updates
  - `DependencyAgeResponse` includes modernization guidance for aging/stale dependencies  
  - `ProjectHealthAnalysis` includes upgrade guidance hints for health issues
- Raw Context7 MCP tools automatically exposed via Spring AI MCP client:
  - `resolve-library-id` - Library search and resolution (when context7.enabled=true)
  - `get-library-docs` - Documentation retrieval with topic queries (when context7.enabled=true)
- Enhanced tool descriptions with Context7 guidance references
- Type-safe record models: `ProjectHealthAnalysis`, `VersionsByType` replacing HashMap usage
- `JacksonConfig` with JDK8 and JSR310 module registration for serialization support

### Changed (1.2.0)

- **Simplified Architecture**: Eliminated complex internal Context7 integration (688 lines removed)
- **Tool Consolidation**: Reduced from 10 to 8 tools for better usability:
  - Enhanced `get_latest_version` to replace `get_stable_version` with `preferStable` parameter
  - Enhanced `check_multiple_dependencies` to replace `check_multiple_stable_versions` with `stableOnly` parameter
- **Guided Delegation**: Maven tools now provide Context7 guidance hints instead of internal documentation calls
- **Conditional Context7 Guidance**: Guidance hints only included when `context7.enabled=true` (enabled by default)
- Removed Context7 parameters from Maven tools (includeMigrationGuidance, includeUpgradeStrategy, includeModernizationGuidance)
- Context7 integration enabled by default (context7.enabled=true) with clean responses when disabled
- Updated tool descriptions to reference separate Context7 tool usage for documentation needs

### Fixed (1.2.0)

- Eliminated Context7 data quality issues through guided delegation approach
- Simplified maintenance by removing complex MCP client orchestration
- Enhanced transparency - LLMs can adapt when Context7 returns incorrect results

### Removed (1.2.0)

- **Complex Context7 Integration**: Removed 688 lines of internal Context7 integration code:
  - `DocumentationEnrichmentService` (replaced with guided delegation)
  - `DocumentationEnrichmentProperties` configuration
  - Complex MCP client orchestration and error handling
- **Context7 Tool Parameters**: Removed from Maven tool method signatures:
  - `includeMigrationGuidance` parameter
  - `includeUpgradeStrategy` parameter  
  - `includeModernizationGuidance` parameter
- **Deprecated Tools**: Removed redundant tools to reduce cognitive overload:
  - `get_stable_version` (functionality moved to `get_latest_version` with `preferStable=true`)
  - `check_multiple_stable_versions` (functionality moved to `check_multiple_dependencies` with `stableOnly=true`)

## [1.1.1] - 2025-07-23

### Fixed (1.1.1)

- Native image configuration for analytical intelligence models
- Added reflection hints for all v1.1.0 analytical model classes (DependencyAgeAnalysis, ReleasePatternAnalysis, VersionTimelineAnalysis)
- Proper native image compatibility for new analytical features

## [1.1.0] - 2025-07-23

### Added (1.1.0)

- **Analytical Intelligence Tools**: Four new MCP tools for advanced dependency analysis
  - `analyze_dependency_age` - Classify dependencies as fresh/current/aging/stale with actionable insights
  - `analyze_release_patterns` - Analyze maintenance activity, release velocity, and predict next releases
  - `get_version_timeline` - Enhanced version timeline with temporal analysis and release gap detection
  - `analyze_project_health` - Comprehensive health scoring for multiple dependencies with risk assessment
- **Enhanced MavenCentralService**: Added timestamp-aware methods for age analysis (`getAllVersionsWithTimestamps`, `getRecentVersionsWithTimestamps`)
- **New Model Classes**: Added comprehensive analytical data structures (`DependencyAgeAnalysis`, `ReleasePatternAnalysis`, `VersionTimelineAnalysis`)
- **Virtual Thread Support**: Concurrent bulk analysis for improved performance
- **New Parameters**: Added analytical parameters (`maxAgeInDays`, `monthsToAnalyze`, `versionCount`)

### Changed (1.1.0)

- **Enhanced Tool Descriptions**: Updated existing tool descriptions for better clarity and universal JVM build tool support
- **Improved Documentation**: Updated README.md and CLAUDE.md with analytical intelligence examples and scope decisions
- **User-Agent Header**: Updated to Maven-Tools-MCP/1.1.0

### Performance

- **Bulk Analysis**: Added concurrent processing for multiple dependency health analysis
- **Caching**: Analytical tools leverage existing 24-hour cache infrastructure
- **Memory Optimization**: Efficient data structures for timeline and pattern analysis

## [1.0.0] - 2025-07-23

### Breaking Changes

This major release updates tool names and adds stability parameters while maintaining compatibility with all JVM build tools.

### ⚠️ BREAKING CHANGES

**Tool Renaming for Universal Appeal:**

- `maven_get_latest` → `get_latest_version`
- `maven_get_stable` → `get_stable_version`
- `maven_check_exists` → `check_version_exists`
- `maven_bulk_check_latest` → `check_multiple_dependencies`
- `maven_bulk_check_stable` → `check_multiple_stable_versions`
- `maven_compare_versions` → `compare_dependency_versions`

### Added (1.0.0)

**New Tool Parameters:**

- `preferStable` parameter for `get_latest_version` - prioritizes stable versions in comprehensive analysis
- `stableOnly` parameter for `check_multiple_dependencies` - filters to production-ready versions only
- `onlyStableTargets` parameter for `compare_dependency_versions` - only suggests stable upgrades for production safety

**JVM Build Tool Support:**

- Support for Maven, Gradle, SBT, Mill, and any JVM build tool
- Standard Maven coordinate format for all tools
- Cross-platform examples and documentation

**Stability Controls:**

- Stability preference controls across all tools
- Filtering options for production deployments
- Upgrade safety controls

### Changed (1.0.0)

**Documentation Updates:**

- Application name remains `maven-tools-mcp` for consistency
- Tool descriptions updated for JVM ecosystem support
- Multi-build tool examples and scenarios
- Examples include Kotlin, Scala, Retrofit, Spark dependencies

**README Updates:**

- Build tool support matrix
- Usage examples with stability controls
- Multi-build tool use cases
- Production deployment examples

**Technical:**

- Updated tool method names and parameters
- Version updated to 1.0.0
- Test suite updated for new signatures

## [0.1.3] - 2025-06-30

### Added (0.1.3)

- Comprehensive version info in dependency check results and improved JSON serialization
- Support for multiple builder platforms in Docker configuration (native AMD64 and ARM64 builds)
- Helpful hints and format examples in tool descriptions for better LLM guidance
- Demo GIF and improved documentation for setup and usage

### Changed (0.1.3)

- Refactored and clarified README with detailed command descriptions, examples, and improved user guidance
- Upgraded Spring Boot to 3.5.3
- Internal refactoring for maintainability and clarity
- Improved formatting and readability of tool descriptions

### Fixed (0.1.3)

- Docker build and manifest creation for multi-architecture images
- Build scripts and Docker image configuration for reliability and compatibility
- Response structure for bulk and compare tools

## [0.1.2] - 2025-06-09

### Added (0.1.2)

- Docker support for MCP server deployment with pre-built images and Docker Compose

### Changed (0.1.2)

- Enhanced build tooling and documentation for Docker deployment
- Use maven-artifact for version comparisons

## [0.1.1] - 2025-06-08

### Added (0.1.1)

- Virtual thread support for optimal I/O-bound performance in bulk operations

### Changed (0.1.1)

- Extended cache TTL from 1 hour to 24 hours for optimal performance

### Removed (0.1.1)

- Support for 'snapshot' version type in all tools, API responses, and documentation. Only stable, rc, beta, alpha, and milestone are now supported.
  - Reason: The underlying Maven Central API used for dependency search does not manage or return snapshot versions, so accurate and reliable snapshot support is not possible.
- All code, documentation, and tests for the `maven_analyze_pom` tool (POM file analysis) have been removed.
  - Reason: Full POM analysis is not efficient or LLM-friendly, and is out of scope for this project. Only direct dependency/version tools are supported.

## [0.1.0] - 2025-06-07

### Added (0.1.0)

- Initial release
- MCP tools for Maven dependency management:
  - `maven_get_latest` - Get latest version (stable, rc, beta, alpha, milestone)
  - `maven_check_exists` - Check if version exists
  - `maven_get_stable` - Get latest stable version
  - `maven_bulk_check_latest` - Bulk version checking
  - `maven_bulk_check_stable` - Bulk stable version checking
  - `maven_analyze_pom` - POM file analysis
  - `maven_compare_versions` - Version comparison
- Caching with 1 hour TTL
- Version classification (stable, rc, beta, alpha, milestone)
- Works with Claude Desktop and GitHub Copilot

### Technical (0.1.0)

- Java 24, Spring Boot 3.5.4, Spring AI
- MCP Protocol 2024-11-05
- Unit and integration tests
- Maven Central API integration

[Unreleased]: https://github.com/arvindand/maven-tools-mcp/compare/v1.4.0...HEAD
[1.4.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/arvindand/maven-tools-mcp/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.3...v1.0.0
[0.1.3]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/arvindand/maven-tools-mcp/releases/tag/v0.1.0

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/service/MavenDependencyTools.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.service;

import com.arvindand.mcp.maven.config.Context7Properties;
import com.arvindand.mcp.maven.model.BulkCheckResult;
import com.arvindand.mcp.maven.model.DependencyAge;
import com.arvindand.mcp.maven.model.DependencyAgeAnalysis;
import com.arvindand.mcp.maven.model.DependencyInfo;
import com.arvindand.mcp.maven.model.MavenArtifact;
import com.arvindand.mcp.maven.model.MavenCoordinate;
import com.arvindand.mcp.maven.model.ProjectHealthAnalysis;
import com.arvindand.mcp.maven.model.ReleasePatternAnalysis;
import com.arvindand.mcp.maven.model.StabilityFilter;
import com.arvindand.mcp.maven.model.ToolResponse;
import com.arvindand.mcp.maven.model.VersionComparison;
import com.arvindand.mcp.maven.model.VersionInfo;
import com.arvindand.mcp.maven.model.VersionInfo.VersionType;
import com.arvindand.mcp.maven.model.VersionTimelineAnalysis;
import com.arvindand.mcp.maven.model.VersionTimelineAnalysis.RecentActivity.ActivityLevel;
import com.arvindand.mcp.maven.model.VersionTimelineAnalysis.TimelineEntry.ReleaseGap;
import com.arvindand.mcp.maven.model.VersionTimelineAnalysis.VelocityTrend.TrendDirection;
import com.arvindand.mcp.maven.model.VersionsByType;
import com.arvindand.mcp.maven.util.MavenCoordinateParser;
import com.arvindand.mcp.maven.util.VersionComparator;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;

/**
 * Main service providing MCP tools for Maven dependency management.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
@Service
public class MavenDependencyTools {

  private static final String UNEXPECTED_ERROR = "Unexpected error";
  private static final String MAVEN_CENTRAL_ERROR = "Maven Central error: ";
  private static final String INVALID_MAVEN_COORDINATE_FORMAT = "Invalid Maven coordinate format: ";
  private static final String SUCCESS_STATUS = "success";
  private static final String ACTIVE_MAINTENANCE = "active";

  // Health level constants
  private static final String EXCELLENT_HEALTH = "excellent";
  private static final String GOOD_HEALTH = "good";
  private static final String FAIR_HEALTH = "fair";
  private static final String POOR_HEALTH = "poor";

  // Age classification constants
  private static final String FRESH_AGE = "fresh";
  private static final String CURRENT_AGE = "current";
  private static final String AGING_AGE = "aging";
  private static final String STALE_AGE = "stale";
  private static final Logger logger = LoggerFactory.getLogger(MavenDependencyTools.class);

  // Analysis constants
  private static final int DEFAULT_ANALYSIS_MONTHS = 24;
  private static final int DEFAULT_VERSION_COUNT = 20;
  private static final int ACCURATE_TIMESTAMP_VERSION_LIMIT = 30;
  private static final int RECENT_VERSIONS_LIMIT = 10;
  private static final int MILLISECONDS_TO_DAYS = 1000 * 60 * 60 * 24;
  private static final int DAYS_IN_MONTH = 30;

  // Health scoring constants
  private static final int PERFECT_HEALTH_SCORE = 100;
  private static final int CURRENT_VERSION_PENALTY = 10;
  private static final int AGING_VERSION_PENALTY = 30;
  private static final int STALE_VERSION_PENALTY = 60;
  private static final int MODERATE_MAINTENANCE_PENALTY = 15;
  private static final int SLOW_MAINTENANCE_PENALTY = 40;
  private static final int AGE_THRESHOLD_PENALTY = 20;

  // Health classification thresholds
  private static final int EXCELLENT_HEALTH_THRESHOLD = 80;
  private static final int GOOD_HEALTH_THRESHOLD = 65;
  private static final int FAIR_HEALTH_THRESHOLD = 50;

  // Stability analysis constants
  private static final int VERY_HIGH_STABILITY_THRESHOLD = 80;
  private static final int LOW_STABILITY_THRESHOLD = 50;
  private final MavenCentralService mavenCentralService;
  private final VersionComparator versionComparator;
  private final Context7Properties context7Properties;

  public MavenDependencyTools(
      MavenCentralService mavenCentralService,
      VersionComparator versionComparator,
      Context7Properties context7Properties) {
    this.mavenCentralService = mavenCentralService;
    this.versionComparator = versionComparator;
    this.context7Properties = context7Properties;
  }

  /**
   * Common error handling for tool operations that return data objects.
   *
   * @param operation the operation to execute
   * @param <T> the return type
   * @return ToolResponse containing either success result or error
   */
  private <T> ToolResponse executeToolOperation(ToolOperation<T> operation) {
    try {
      T result = operation.execute();
      return ToolResponse.Success.of(result);
    } catch (IllegalArgumentException e) {
      return ToolResponse.Error.of(INVALID_MAVEN_COORDINATE_FORMAT + e.getMessage());
    } catch (MavenCentralException e) {
      return ToolResponse.Error.of(MAVEN_CENTRAL_ERROR + e.getMessage());
    } catch (Exception e) {
      logger.error(UNEXPECTED_ERROR, e);
      return ToolResponse.Error.of(UNEXPECTED_ERROR + ": " + e.getMessage());
    }
  }

  @FunctionalInterface
  private interface ToolOperation<T> {
    T execute() throws IllegalArgumentException, MavenCentralException;
  }

  /**
   * Get the latest version of any dependency from Maven Central (works with Maven, Gradle, SBT,
   * Mill).
   *
   * @param dependency the dependency coordinate (groupId:artifactId)
   * @param stabilityFilter controls version filtering: ALL (default), STABLE_ONLY, or PREFER_STABLE
   * @return JSON response with latest versions by type
   */
  @SuppressWarnings("java:S100") // MCP tool method naming
  @Tool(
      description =
          "Single dependency. Returns newest versions by type (stable/rc/beta/alpha/milestone). Set"
              + " stabilityFilter to ALL (default), STABLE_ONLY, or PREFER_STABLE. Use when asked:"
              + " 'what's the latest version of X?' Works with all JVM build tools.")
  public ToolResponse get_latest_version(
      @ToolParam(
              description =
                  "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
                      + " Example: 'org.springframework:spring-core'")
          String dependency,
      @ToolParam(
              description =
                  "Stability filter: ALL (all versions), STABLE_ONLY (production-ready only), or"
                      + " PREFER_STABLE (prioritize stable, show others too). Default:"
                      + " PREFER_STABLE",
              required = false)
          @Nullable
          StabilityFilter stabilityFilter) {
    return executeToolOperation(
        () -> {
          MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
          List<String> allVersions = mavenCentralService.getAllVersions(coordinate);

          if (allVersions.isEmpty()) {
            return notFoundResponse(coordinate);
          }

          StabilityFilter filter =
              stabilityFilter != null ? stabilityFilter : StabilityFilter.PREFER_STABLE;
          return buildVersionsByType(coordinate, allVersions, filter);
        });
  }

  /**
   * Check if specific dependency version exists and identify its stability type.
   *
   * @param dependency the dependency coordinate (groupId:artifactId)
   * @param version the version to check
   * @return JSON response with existence status and version type
   */
  @SuppressWarnings("java:S100") // MCP tool method naming
  @Tool(
      description =
          "Single dependency + version. Validates existence on Maven Central and classifies its"
              + " stability (stable/rc/beta/alpha/milestone/snapshot). Use when asked: 'does X:Y"
              + " exist?' or 'is version V stable?'")
  public ToolResponse check_version_exists(
      @ToolParam(
              description =
                  "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
                      + " Example: 'org.springframework:spring-core'")
          String dependency,
      @ToolParam(
              description =
                  "Specific version string to check for existence. Example: '6.1.4' or"
                      + " '2.7.18-SNAPSHOT'")
          String version) {
    return executeToolOperation(
        () -> {
          MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
          String versionToCheck = coordinate.version() != null ? coordinate.version() : version;

          if (versionToCheck == null || versionToCheck.trim().isEmpty()) {
            throw new IllegalArgumentException(
                "Version must be provided either in dependency string or version parameter");
          }

          boolean exists = mavenCentralService.checkVersionExists(coordinate, versionToCheck);
          String versionType = versionComparator.getVersionTypeString(versionToCheck);

          return DependencyInfo.success(
              coordinate,
              versionToCheck,
              exists,
              versionType,
              versionComparator.isStableVersion(versionToCheck),
              null);
        });
  }

  /**
   * Check latest versions for multiple dependencies with filtering options.
   *
   * @param dependencies comma or newline separated list of dependency coordinates
   * @param stabilityFilter controls version filtering: ALL, STABLE_ONLY, or PREFER_STABLE
   * @return JSON response with bulk check results
   */
  @SuppressWarnings("java:S100") // MCP tool method naming
  @Tool(
      description =
          "Bulk. For many coordinates (no versions), returns per-dependency latest versions by"
              + " type. Set stabilityFilter to ALL (default), STABLE_ONLY, or PREFER_STABLE."
              + " Use for audits of multiple dependencies.")
  public ToolResponse check_multiple_dependencies(
      @ToolParam(
              description =
                  "Comma or newline separated list of Maven dependency coordinates in format"
                      + " 'groupId:artifactId' (NO versions). Example:"
                      + " 'org.springframework:spring-core,junit:junit'")
          String dependencies,
      @ToolParam(
              description =
                  "Stability filter: ALL (all versions), STABLE_ONLY (production-ready only), or"
                      + " PREFER_STABLE (prioritize stable). Default: ALL",
              required = false)
          @Nullable
          StabilityFilter stabilityFilter) {
    return executeToolOperation(
        () -> {
          List<String> depList = parseDependencies(dependencies);
          StabilityFilter filter = stabilityFilter != null ? stabilityFilter : StabilityFilter.ALL;

          List<BulkCheckResult> results;
          try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<CompletableFuture<BulkCheckResult>> futures =
                depList.stream()
                    .distinct()
                    .map(
                        dep ->
                            CompletableFuture.supplyAsync(
                                () -> processVersionCheck(dep, filter), executor))
                    .toList();
            results = futures.stream().map(CompletableFuture::join).toList();
          }

          return results;
        });
  }

  /**
   * Compare current dependency versions with latest available and show upgrade recommendations.
   *
   * @param currentDependencies comma or newline separated list of dependency coordinates with
   *     versions
   * @param stabilityFilter controls upgrade targets: ALL, STABLE_ONLY, or PREFER_STABLE
   * @return JSON response with version comparison and update recommendations
   */
  @SuppressWarnings("java:S100") // MCP tool method naming
  @Tool(
      description =
          "Bulk compare. Input includes versions. Suggests upgrades and classifies update type"
              + " (major/minor/patch). Set stabilityFilter to ALL (default), STABLE_ONLY, or"
              + " PREFER_STABLE. Never suggests downgrades.")
  public ToolResponse compare_dependency_versions(
      @ToolParam(
              description =
                  "Comma or newline separated list of dependency coordinates WITH versions in"
                      + " format 'groupId:artifactId:version'. Example:"
                      + " 'org.springframework:spring-core:6.0.0,junit:junit:4.12'")
          String currentDependencies,
      @ToolParam(
              description =
                  "Stability filter: ALL (any version), STABLE_ONLY (production-ready only), or"
                      + " PREFER_STABLE (prioritize stable). Default: ALL",
              required = false)
          @Nullable
          StabilityFilter stabilityFilter) {
    return executeToolOperation(
        () -> {
          List<String> depList = parseDependencies(currentDependencies);
          StabilityFilter filter = stabilityFilter != null ? stabilityFilter : StabilityFilter.ALL;

          List<VersionComparison.DependencyComparisonResult> results;
          try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<CompletableFuture<VersionComparison.DependencyComparisonResult>> futures =
                depList.stream()
                    .distinct()
                    .map(
                        dep ->
                            CompletableFuture.supplyAsync(
                                () -> compareDependencyVersion(dep, filter), executor))
                    .toList();
            results = futures.stream().map(CompletableFuture::join).toList();
          }

          VersionComparison.UpdateSummary summary = calculateUpdateSummary(results);
          return new VersionComparison(Instant.now(), results, summary);
        });
  }

  /**
   * Analyze dependency age and freshness classification with actionable insights.
   *
   * @param dependency the dependency coordinate (groupId:artifactId)
   * @param maxAgeInDays optional maximum acceptable age in days (default: no limit)
   * @return JSON response with age analysis and recommendations
   */
  @SuppressWarnings("java:S100") // MCP tool method naming
  @Tool(
      description =
          "Single dependency. Returns days since last release and freshness"
              + " (fresh/current/aging/stale), with actionable insights. Use when asked about 'how"
              + " old' or 'last release' of a library.")
  public ToolResponse analyze_dependency_age(
      @ToolParam(
              description =
                  "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
                      + " Example: 'org.springframework:spring-core'")
          String dependency,
      @ToolParam(
              description =
                  "Optional maximum acceptable age threshold in days. If specified and dependency"
                      + " exceeds this age, additional recommendations are provided. No limit if"
                      + " not specified",
              required = false)
          @Nullable
          Integer maxAgeInDays) {
    return executeToolOperation(
        () -> {
          MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
          List<MavenArtifact> versions =
              mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, 1);

          if (versions.isEmpty()) {
            return notFoundResponse(coordinate);
          }

          MavenArtifact latestVersion = versions.get(0);
          DependencyAgeAnalysis basicAnalysis =
              DependencyAgeAnalysis.fromTimestamp(
                  coordinate.toCoordinateString(),
                  latestVersion.version(),
                  latestVersion.timestamp());

          // Add custom recommendation if maxAgeInDays is specified
          DependencyAgeAnalysis analysis = basicAnalysis;
          if (maxAgeInDays != null && basicAnalysis.daysSinceLastRelease() > maxAgeInDays) {
            analysis =
                new DependencyAgeAnalysis(
                    basicAnalysis.dependency(),
                    basicAnalysis.latestVersion(),
                    basicAnalysis.ageClassification(),
                    basicAnalysis.daysSinceLastRelease(),
                    basicAnalysis.lastReleaseDate(),
                    basicAnalysis.ageDescription(),
                    "Exceeds specified age threshold of "
                        + maxAgeInDays
                        + " days - "
                        + basicAnalysis.recommendation());
          }

          // Create response with basic analysis
          return DependencyAge.from(analysis, context7Properties.enabled());
        });
  }

  /**
   * Analyze release patterns and maintenance activity to predict future releases.
   *
   * @param dependency the dependency coordinate (groupId:artifactId)
   * @param monthsToAnalyze number of months of history to analyze (default: 24)
   * @return JSON response with release pattern analysis and predictions
   */
  @SuppressWarnings("java:S100") // MCP tool method naming
  @Tool(
      description =
          "Single dependency. Analyzes historical releases to infer cadence, consistency, and"
              + " likely next-release timeframe. Useful for maintenance and planning.")
  public ToolResponse analyze_release_patterns(
      @ToolParam(
              description =
                  "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
                      + " Example: 'com.fasterxml.jackson.core:jackson-core'")
          String dependency,
      @ToolParam(
              description =
                  "Number of months of historical release data to analyze for patterns and"
                      + " predictions. Default is 24 months if not specified",
              required = false)
          @Nullable
          Integer monthsToAnalyze) {
    return executeToolOperation(
        () -> {
          MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
          int analysisMonths = monthsToAnalyze != null ? monthsToAnalyze : DEFAULT_ANALYSIS_MONTHS;

          List<MavenArtifact> allVersions =
              mavenCentralService.getRecentVersionsWithAccurateTimestamps(
                  coordinate, ACCURATE_TIMESTAMP_VERSION_LIMIT);

          if (allVersions.isEmpty()) {
            throw new MavenCentralException(
                "No versions found for " + coordinate.toCoordinateString());
          }

          return analyzeReleasePattern(
              coordinate.toCoordinateString(), allVersions, analysisMonths);
        });
  }

  /**
   * Get enhanced version timeline with temporal analysis and release patterns.
   *
   * @param dependency the dependency coordinate (groupId:artifactId)
   * @param versionCount number of recent versions to include (default: 20)
   * @return JSON response with version timeline and temporal insights
   */
  @SuppressWarnings("java:S100") // MCP tool method naming
  @Tool(
      description =
          "Single dependency. Returns a timeline of recent versions with dates, gaps, and stability"
              + " patterns. Use for quick release history snapshots.")
  public ToolResponse get_version_timeline(
      @ToolParam(
              description =
                  "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
                      + " Example: 'org.junit.jupiter:junit-jupiter'")
          String dependency,
      @ToolParam(
              description =
                  "Number of recent versions to include in timeline analysis. Default is 20"
                      + " versions if not specified. Typical range: 10-50",
              required = false)
          @Nullable
          Integer versionCount) {
    return executeToolOperation(
        () -> {
          MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
          int maxVersions = versionCount != null ? versionCount : DEFAULT_VERSION_COUNT;

          List<MavenArtifact> versions =
              mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, maxVersions);

          if (versions.isEmpty()) {
            throw new MavenCentralException(
                "No versions found for " + coordinate.toCoordinateString());
          }

          return analyzeVersionTimeline(coordinate.toCoordinateString(), versions);
        });
  }

  /**
   * Analyze overall health of multiple dependencies with age and maintenance insights.
   *
   * @param dependencies comma or newline separated list of dependency coordinates
   * @param maxAgeInDays optional maximum acceptable age in days for health scoring
   * @param stabilityFilter controls recommendations: ALL, STABLE_ONLY, or PREFER_STABLE
   * @return JSON response with project health summary and individual dependency analysis
   */
  @SuppressWarnings("java:S100") // MCP tool method naming
  @Tool(
      description =
          "Bulk project view. Summarizes health across many dependencies using age and maintenance"
              + " patterns, with concise recommendations. Set stabilityFilter to ALL (default),"
              + " STABLE_ONLY, or PREFER_STABLE for upgrade recommendations.")
  public ToolResponse analyze_project_health(
      @ToolParam(
              description =
                  "Comma or newline separated list of Maven dependency coordinates in format"
                      + " 'groupId:artifactId' (NO versions). Example:"
                      + " 'org.springframework:spring-core,junit:junit'")
          String dependencies,
      @ToolParam(
              description =
                  "Optional maximum acceptable age threshold in days for health scoring."
                      + " Dependencies exceeding this age receive lower health scores. No age"
                      + " penalty if not specified",
              required = false)
          @Nullable
          Integer maxAgeInDays,
      @ToolParam(
              description =
                  "Stability filter: ALL (any version), STABLE_ONLY (production-ready only), or"
                      + " PREFER_STABLE (prioritize stable). Default: PREFER_STABLE",
              required = false)
          @Nullable
          StabilityFilter stabilityFilter) {
    return executeToolOperation(
        () -> {
          List<String> depList = parseDependencies(dependencies);

          if (depList.isEmpty()) {
            throw new IllegalArgumentException("No dependencies provided for analysis");
          }

          // Analyze each dependency for age and patterns
          List<ProjectHealthAnalysis.DependencyHealthAnalysis> dependencyAnalyses;
          try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<CompletableFuture<ProjectHealthAnalysis.DependencyHealthAnalysis>> futures =
                depList.stream()
                    .distinct()
                    .map(
                        dep ->
                            CompletableFuture.supplyAsync(
                                () -> analyzeSimpleDependencyHealth(dep, maxAgeInDays), executor))
                    .toList();
            dependencyAnalyses = futures.stream().map(CompletableFuture::join).toList();
          }

          return buildSimpleHealthSummary(dependencyAnalyses, maxAgeInDays);
        });
  }

  private ToolResponse notFoundResponse(MavenCoordinate coordinate) {
    String message =
        "No Maven dependency found for %s:%s%s"
            .formatted(
                coordinate.groupId(),
                coordinate.artifactId(),
                coordinate.packaging() != null ? ":" + coordinate.packaging() : "");
    return ToolResponse.Error.notFound(message);
  }

  @SuppressWarnings("java:S1172") // preferStable is used by VersionsByType.getPreferredVersion()
  private VersionsByType buildVersionsByType(
      MavenCoordinate coordinate, List<String> allVersions, StabilityFilter stabilityFilter) {
    Map<VersionType, String> versionsByType = HashMap.newHashMap(5);

    for (String version : allVersions) {
      VersionType type = versionComparator.getVersionType(version);
      versionsByType.putIfAbsent(type, version);
      if (versionsByType.size() == 5) break;
    }

    return VersionsByType.create(
        coordinate.toCoordinateString(),
        createVersionInfo(versionsByType.get(VersionType.STABLE)),
        createVersionInfo(versionsByType.get(VersionType.RC)),
        createVersionInfo(versionsByType.get(VersionType.BETA)),
        createVersionInfo(versionsByType.get(VersionType.ALPHA)),
        createVersionInfo(versionsByType.get(VersionType.MILESTONE)),
        allVersions.size());
  }

  private Optional<VersionInfo> createVersionInfo(String version) {
    return version != null
        ? Optional.of(new VersionInfo(version, versionComparator.getVersionType(version)))
        : Optional.empty();
  }

  private List<String> parseDependencies(String dependencies) {
    if (dependencies == null || dependencies.trim().isEmpty()) {
      return List.of();
    }

    return dependencies
        .lines()
        .flatMap(line -> Arrays.stream(line.split(",")))
        .map(String::trim)
        .filter(dep -> !dep.isEmpty())
        .toList();
  }

  private BulkCheckResult processVersionCheck(String dep, StabilityFilter filter) {
    return switch (filter) {
      case STABLE_ONLY -> processStableVersionCheck(dep);
      case ALL, PREFER_STABLE -> processComprehensiveVersionCheck(dep);
    };
  }

  private BulkCheckResult processStableVersionCheck(String dep) {
    try {
      MavenCoordinate coordinate = MavenCoordinateParser.parse(dep);
      List<String> allVersions = mavenCentralService.getAllVersions(coordinate);

      if (allVersions.isEmpty()) {
        return BulkCheckResult.notFound(coordinate.toCoordinateString());
      }

      List<String> stableVersions =
          allVersions.stream().filter(versionComparator::isStableVersion).toList();

      String latestStable = stableVersions.isEmpty() ? null : stableVersions.get(0);

      return latestStable != null
          ? BulkCheckResult.foundStable(
              coordinate.toCoordinateString(),
              latestStable,
              VersionType.STABLE.getDisplayName(),
              allVersions.size(),
              stableVersions.size())
          : BulkCheckResult.noStableVersion(coordinate.toCoordinateString(), allVersions.size());
    } catch (Exception e) {
      return BulkCheckResult.error(dep, e.getMessage());
    }
  }

  private VersionComparison.DependencyComparisonResult compareDependencyVersion(
      String dep, StabilityFilter stabilityFilter) {
    try {
      MavenCoordinate coordinate = MavenCoordinateParser.parse(dep);
      String currentVersion = coordinate.version();
      String latestVersion =
          stabilityFilter == StabilityFilter.STABLE_ONLY
              ? getLatestStableVersion(coordinate)
              : mavenCentralService.getLatestVersion(coordinate);

      if (latestVersion == null) {
        return VersionComparison.DependencyComparisonResult.notFound(
            coordinate.toCoordinateString(), currentVersion);
      }

      if (currentVersion == null) {
        return VersionComparison.DependencyComparisonResult.noCurrentVersion(
            coordinate.toCoordinateString());
      }

      String latestType = versionComparator.getVersionTypeString(latestVersion);
      String updateType = versionComparator.determineUpdateType(currentVersion, latestVersion);
      boolean updateAvailable = versionComparator.compare(currentVersion, latestVersion) < 0;

      // Return basic comparison result

      return VersionComparison.DependencyComparisonResult.success(
          coordinate.toCoordinateString(),
          currentVersion,
          latestVersion,
          latestType,
          updateType,
          updateAvailable,
          context7Properties.enabled());
    } catch (Exception e) {
      return VersionComparison.DependencyComparisonResult.error(dep, e.getMessage());
    }
  }

  private BulkCheckResult processComprehensiveVersionCheck(String dep) {
    try {
      MavenCoordinate coordinate = MavenCoordinateParser.parse(dep);
      List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
      if (allVersions.isEmpty()) {
        return BulkCheckResult.notFound(dep);
      }

      Map<VersionType, String> versionsByType = buildVersionsByType(allVersions);
      VersionInfoCollection versionInfos = createVersionInfoCollection(versionsByType);
      int stableCount = countStableVersions(allVersions);

      String primaryVersion =
          versionInfos.latestStable() != null
              ? versionInfos.latestStable().version()
              : allVersions.get(0);
      String primaryType =
          versionInfos.latestStable() != null
              ? VersionType.STABLE.getDisplayName()
              : versionComparator.getVersionTypeString(allVersions.get(0));

      return BulkCheckResult.foundComprehensive(
          dep,
          primaryVersion,
          primaryType,
          allVersions.size(),
          stableCount,
          versionInfos.latestStable(),
          versionInfos.latestRc(),
          versionInfos.latestBeta(),
          versionInfos.latestAlpha(),
          versionInfos.latestMilestone());
    } catch (Exception e) {
      logger.error("Error processing comprehensive version check for {}: {}", dep, e.getMessage());
      return BulkCheckResult.error(dep, e.getMessage());
    }
  }

  private Map<VersionType, String> buildVersionsByType(List<String> allVersions) {
    Map<VersionType, String> versionsByType = HashMap.newHashMap(5);
    Set<VersionType> remainingTypes = EnumSet.allOf(VersionType.class);

    for (String version : allVersions) {
      VersionType type = versionComparator.getVersionType(version);
      if (remainingTypes.contains(type)) {
        versionsByType.putIfAbsent(type, version);
        remainingTypes.remove(type);
        if (remainingTypes.isEmpty()) break;
      }
    }
    return versionsByType;
  }

  private VersionInfoCollection createVersionInfoCollection(
      Map<VersionType, String> versionsByType) {
    return new VersionInfoCollection(
        createVersionInfo(versionsByType, VersionType.STABLE),
        createVersionInfo(versionsByType, VersionType.RC),
        createVersionInfo(versionsByType, VersionType.BETA),
        createVersionInfo(versionsByType, VersionType.ALPHA),
        createVersionInfo(versionsByType, VersionType.MILESTONE));
  }

  private VersionInfo createVersionInfo(Map<VersionType, String> versionsByType, VersionType type) {
    return versionsByType.containsKey(type)
        ? new VersionInfo(versionsByType.get(type), type)
        : null;
  }

  private int countStableVersions(List<String> allVersions) {
    return allVersions.stream()
        .mapToInt(v -> versionComparator.getVersionType(v) == VersionType.STABLE ? 1 : 0)
        .sum();
  }

  private record VersionInfoCollection(
      VersionInfo latestStable,
      VersionInfo latestRc,
      VersionInfo latestBeta,
      VersionInfo latestAlpha,
      VersionInfo latestMilestone) {}

  private VersionComparison.UpdateSummary calculateUpdateSummary(
      List<VersionComparison.DependencyComparisonResult> results) {

    Map<String, Long> counts =
        results.stream()
            .filter(result -> SUCCESS_STATUS.equals(result.status()))
            .collect(
                Collectors.groupingBy(
                    VersionComparison.DependencyComparisonResult::updateType,
                    Collectors.counting()));

    return new VersionComparison.UpdateSummary(
        counts.getOrDefault("major", 0L).intValue(),
        counts.getOrDefault("minor", 0L).intValue(),
        counts.getOrDefault("patch", 0L).intValue(),
        counts.getOrDefault("none", 0L).intValue());
  }

  private String getLatestStableVersion(MavenCoordinate coordinate) throws MavenCentralException {
    List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
    List<String> stableVersions =
        allVersions.stream().filter(versionComparator::isStableVersion).toList();
    return stableVersions.isEmpty() ? null : stableVersions.get(0);
  }

  private ReleasePatternAnalysis analyzeReleasePattern(
      String dependency, List<MavenArtifact> allVersions, int analysisMonths) {

    Instant now = Instant.now();
    Instant cutoffDate = now.minus((long) analysisMonths * DAYS_IN_MONTH, ChronoUnit.DAYS);

    // Filter versions within analysis period
    List<MavenArtifact> analysisVersions =
        allVersions.stream()
            .filter(
                v -> {
                  Instant releaseDate = Instant.ofEpochMilli(v.timestamp());
                  return releaseDate.isAfter(cutoffDate);
                })
            .toList();

    if (analysisVersions.isEmpty()) {
      // Fallback to all versions if none in analysis period
      analysisVersions = allVersions.stream().limit(RECENT_VERSIONS_LIMIT).toList();
    }

    // Calculate release intervals
    List<Long> intervals = new ArrayList<>();
    for (int i = 1; i < analysisVersions.size(); i++) {
      long prevTimestamp = analysisVersions.get(i).timestamp();
      long currentTimestamp = analysisVersions.get(i - 1).timestamp();
      long intervalDays = (currentTimestamp - prevTimestamp) / MILLISECONDS_TO_DAYS;
      if (intervalDays > 0) intervals.add(intervalDays);
    }

    // Calculate statistics in single pass
    double averageDays;
    long maxInterval;
    long minInterval;
    if (intervals.isEmpty()) {
      averageDays = 0;
      maxInterval = 0;
      minInterval = 0;
    } else {
      var stats = intervals.stream().mapToLong(Long::longValue).summaryStatistics();
      averageDays = stats.getAverage();
      maxInterval = stats.getMax();
      minInterval = stats.getMin();
    }
    double releaseVelocity = averageDays > 0 ? (DAYS_IN_MONTH / averageDays) : 0;

    Instant lastReleaseDate = Instant.ofEpochMilli(analysisVersions.get(0).timestamp());
    long daysSinceLastRelease = Duration.between(lastReleaseDate, now).toDays();

    // Classifications
    ReleasePatternAnalysis.MaintenanceLevel maintenanceLevel =
        ReleasePatternAnalysis.MaintenanceLevel.classify(releaseVelocity, daysSinceLastRelease);
    ReleasePatternAnalysis.ReleaseConsistency consistency =
        ReleasePatternAnalysis.ReleaseConsistency.classify(averageDays, maxInterval, minInterval);

    // Build recent releases info
    List<ReleasePatternAnalysis.ReleaseInfo> recentReleases =
        analysisVersions.stream()
            .limit(10)
            .map(
                v -> {
                  Instant releaseDate = Instant.ofEpochMilli(v.timestamp());
                  return new ReleasePatternAnalysis.ReleaseInfo(v.version(), releaseDate, null);
                })
            .toList();

    String nextReleasePrediction =
        ReleasePatternAnalysis.predictNextRelease(averageDays, daysSinceLastRelease, consistency);
    String recommendation = ReleasePatternAnalysis.generateRecommendation(maintenanceLevel);

    return new ReleasePatternAnalysis(
        dependency,
        analysisVersions.size(),
        analysisMonths,
        averageDays,
        releaseVelocity,
        maintenanceLevel,
        consistency,
        lastReleaseDate,
        nextReleasePrediction,
        recentReleases,
        recommendation);
  }

  private VersionTimelineAnalysis analyzeVersionTimeline(
      String dependency, List<MavenArtifact> versions) {

    Instant now = Instant.now();

    // Pre-calculate all intervals and average - single pass optimization
    long[] intervalDays = new long[versions.size()];
    List<Long> positiveIntervals = new ArrayList<>();

    for (int i = 1; i < versions.size(); i++) {
      long currentTimestamp = versions.get(i - 1).timestamp();
      long prevTimestamp = versions.get(i).timestamp();
      long interval = (currentTimestamp - prevTimestamp) / MILLISECONDS_TO_DAYS;
      intervalDays[i] = interval;
      if (interval > 0) positiveIntervals.add(interval);
    }

    double averageInterval =
        positiveIntervals.isEmpty()
            ? 0
            : positiveIntervals.stream().mapToLong(Long::longValue).average().orElse(0);

    // Build timeline entries using pre-calculated intervals
    List<VersionTimelineAnalysis.TimelineEntry> timeline = new ArrayList<>();
    for (int i = 0; i < versions.size(); i++) {
      MavenArtifact version = versions.get(i);
      Instant releaseDate = Instant.ofEpochMilli(version.timestamp());

      String relativeTime = VersionTimelineAnalysis.formatRelativeTime(releaseDate, now);
      VersionType versionType = versionComparator.getVersionType(version.version());

      Long daysSincePrevious = i > 0 ? intervalDays[i] : null;
      ReleaseGap gap =
          i > 0 ? ReleaseGap.classify(intervalDays[i], averageInterval) : ReleaseGap.NORMAL;

      boolean isBreakingChange =
          versionComparator.determineUpdateType("0.0.0", version.version()).equals("major");

      timeline.add(
          new VersionTimelineAnalysis.TimelineEntry(
              version.version(),
              versionType,
              releaseDate,
              relativeTime,
              daysSincePrevious,
              isBreakingChange,
              gap));
    }

    // Calculate metrics
    Instant oldestDate = Instant.ofEpochMilli(versions.get(versions.size() - 1).timestamp());
    int timeSpanMonths = (int) Duration.between(oldestDate, now).toDays() / DAYS_IN_MONTH;

    // Count recent activity with optimized stream operations
    Instant oneMonthAgo = now.minus(DAYS_IN_MONTH, ChronoUnit.DAYS);
    Instant threeMonthsAgo = now.minus(3L * DAYS_IN_MONTH, ChronoUnit.DAYS);

    long releasesLastMonth =
        versions.stream()
            .filter(v -> Instant.ofEpochMilli(v.timestamp()).isAfter(oneMonthAgo))
            .count();

    long releasesLastQuarter =
        versions.stream()
            .filter(v -> Instant.ofEpochMilli(v.timestamp()).isAfter(threeMonthsAgo))
            .count();

    // Create analysis objects
    VersionTimelineAnalysis.VelocityTrend velocityTrend =
        new VersionTimelineAnalysis.VelocityTrend(
            TrendDirection.STABLE,
            "Release velocity appears stable",
            releasesLastQuarter / 3.0,
            versions.size() / Math.max(timeSpanMonths, 1.0),
            0.0);

    long stableCount =
        timeline.stream().mapToLong(t -> t.versionType() == VersionType.STABLE ? 1 : 0).sum();
    double stablePercentage = (double) stableCount / timeline.size() * 100;

    VersionTimelineAnalysis.StabilityPattern stabilityPattern =
        new VersionTimelineAnalysis.StabilityPattern(
            stablePercentage,
            "Mix of stable and pre-release versions",
            "Regular stable releases",
            stablePercentage > 70
                ? "Good stability pattern - safe for production use"
                : "Consider waiting for stable releases");

    long lastReleaseAge = Duration.between(timeline.get(0).releaseDate(), now).toDays();

    VersionTimelineAnalysis.RecentActivity recentActivity =
        new VersionTimelineAnalysis.RecentActivity(
            (int) releasesLastMonth,
            (int) releasesLastQuarter,
            ActivityLevel.classify((int) releasesLastMonth, (int) releasesLastQuarter),
            lastReleaseAge,
            "Recent activity: " + releasesLastQuarter + " releases in last quarter");

    List<String> insights = generateTimelineInsights(timeline, recentActivity, stabilityPattern);

    return new VersionTimelineAnalysis(
        dependency,
        versions.size(),
        timeline.size(),
        timeSpanMonths,
        timeline,
        velocityTrend,
        stabilityPattern,
        recentActivity,
        insights);
  }

  private List<String> generateTimelineInsights(
      List<VersionTimelineAnalysis.TimelineEntry> timeline,
      VersionTimelineAnalysis.RecentActivity recentActivity,
      VersionTimelineAnalysis.StabilityPattern stabilityPattern) {

    List<String> insights = new ArrayList<>();

    if (recentActivity.activityLevel() == ActivityLevel.VERY_ACTIVE) {
      insights.add("High release frequency indicates active development");
    } else if (recentActivity.activityLevel() == ActivityLevel.DORMANT) {
      insights.add("No recent releases - consider checking project status");
    }

    if (stabilityPattern.stablePercentage() > VERY_HIGH_STABILITY_THRESHOLD) {
      insights.add("Strong preference for stable releases - good for production");
    } else if (stabilityPattern.stablePercentage() < LOW_STABILITY_THRESHOLD) {
      insights.add("Many pre-release versions - early-stage or experimental project");
    }

    long majorGaps =
        timeline.stream().mapToLong(t -> t.releaseGap() == ReleaseGap.MAJOR_GAP ? 1 : 0).sum();
    if (majorGaps > 0) {
      insights.add("Found " + majorGaps + " significant gaps in release schedule");
    }

    return insights;
  }

  private ProjectHealthAnalysis.DependencyHealthAnalysis analyzeSimpleDependencyHealth(
      String dependency, Integer maxAgeInDays) {
    try {
      MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
      List<MavenArtifact> versions =
          mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, 10);

      if (versions.isEmpty()) {
        return ProjectHealthAnalysis.DependencyHealthAnalysis.notFound(dependency);
      }

      MavenArtifact latestVersion = versions.get(0);
      DependencyAgeAnalysis ageAnalysis =
          DependencyAgeAnalysis.fromTimestamp(
              coordinate.toCoordinateString(), latestVersion.version(), latestVersion.timestamp());

      // Simple maintenance assessment based on recent versions
      Instant now = Instant.now();
      Instant sixMonthsAgo = now.minus(6L * DAYS_IN_MONTH, ChronoUnit.DAYS);

      long recentVersions =
          versions.stream()
              .filter(v -> Instant.ofEpochMilli(v.timestamp()).isAfter(sixMonthsAgo))
              .count();

      String maintenanceStatus;
      if (recentVersions >= 3) {
        maintenanceStatus = ACTIVE_MAINTENANCE;
      } else if (recentVersions >= 1) {
        maintenanceStatus = "moderate";
      } else {
        maintenanceStatus = "slow";
      }

      // Health score (0-100)
      int healthScore = calculateSimpleHealthScore(ageAnalysis, maintenanceStatus, maxAgeInDays);

      // No upgrade strategy in basic analysis

      return ProjectHealthAnalysis.DependencyHealthAnalysis.success(
          dependency,
          latestVersion.version(),
          ageAnalysis.ageClassification().getName(),
          ageAnalysis.daysSinceLastRelease(),
          healthScore,
          maintenanceStatus,
          context7Properties.enabled());
    } catch (Exception e) {
      return ProjectHealthAnalysis.DependencyHealthAnalysis.error(dependency, e.getMessage());
    }
  }

  private int calculateSimpleHealthScore(
      DependencyAgeAnalysis ageAnalysis, String maintenanceStatus, Integer maxAgeInDays) {

    int score = PERFECT_HEALTH_SCORE;

    // Age penalty
    switch (ageAnalysis.ageClassification()) {
      case FRESH -> score -= 0; // No penalty
      case CURRENT -> score -= CURRENT_VERSION_PENALTY;
      case AGING -> score -= AGING_VERSION_PENALTY;
      case STALE -> score -= STALE_VERSION_PENALTY;
    }

    // Maintenance penalty
    switch (maintenanceStatus) {
      case ACTIVE_MAINTENANCE -> score -= 0; // No penalty
      case "moderate" -> score -= MODERATE_MAINTENANCE_PENALTY;
      case "slow" -> score -= SLOW_MAINTENANCE_PENALTY;
      default -> {
        // Unknown maintenance status - no penalty
      }
    }

    // Custom age threshold penalty
    if (maxAgeInDays != null && ageAnalysis.daysSinceLastRelease() > maxAgeInDays) {
      score -= AGE_THRESHOLD_PENALTY;
    }

    return Math.max(0, score);
  }

  private ProjectHealthAnalysis buildSimpleHealthSummary(
      List<ProjectHealthAnalysis.DependencyHealthAnalysis> dependencyAnalyses,
      Integer maxAgeInDays) {

    int totalDependencies = dependencyAnalyses.size();
    int successfulAnalyses =
        (int)
            dependencyAnalyses.stream()
                .mapToLong(dep -> SUCCESS_STATUS.equals(dep.status()) ? 1 : 0)
                .sum();

    // Calculate averages and counts in single pass
    List<ProjectHealthAnalysis.DependencyHealthAnalysis> successfulDeps =
        dependencyAnalyses.stream().filter(dep -> SUCCESS_STATUS.equals(dep.status())).toList();

    if (successfulDeps.isEmpty()) {
      return new ProjectHealthAnalysis(
          "unknown",
          0,
          totalDependencies,
          0,
          new ProjectHealthAnalysis.AgeDistribution(0, 0, 0, 0),
          dependencyAnalyses,
          List.of("Unable to analyze any dependencies"));
    }

    // Single pass through successful dependencies
    DependencyMetrics metrics = calculateDependencyMetrics(successfulDeps);

    double averageHealthScore = metrics.totalHealthScore() / (double) successfulDeps.size();
    String overallHealth = determineOverallHealth(averageHealthScore);

    // Create age distribution
    ProjectHealthAnalysis.AgeDistribution ageDistribution =
        new ProjectHealthAnalysis.AgeDistribution(
            (int) metrics.freshCount(),
            (int) metrics.currentCount(),
            (int) metrics.agingCount(),
            (int) metrics.staleCount());

    // Generate recommendations
    List<String> recommendations =
        generateHealthRecommendations(metrics, successfulAnalyses, successfulDeps, maxAgeInDays);

    return new ProjectHealthAnalysis(
        overallHealth,
        (int) Math.round(averageHealthScore),
        totalDependencies,
        successfulAnalyses,
        ageDistribution,
        dependencyAnalyses,
        recommendations);
  }

  private DependencyMetrics calculateDependencyMetrics(
      List<ProjectHealthAnalysis.DependencyHealthAnalysis> successfulDeps) {
    // Calculate all metrics in optimized stream operations
    int totalHealthScore =
        successfulDeps.stream()
            .mapToInt(ProjectHealthAnalysis.DependencyHealthAnalysis::healthScore)
            .sum();

    Map<String, Long> ageCounts =
        successfulDeps.stream()
            .collect(
                Collectors.groupingBy(
                    ProjectHealthAnalysis.DependencyHealthAnalysis::ageClassification,
                    Collectors.counting()));

    long activeMaintenanceCount =
        successfulDeps.stream()
            .filter(dep -> ACTIVE_MAINTENANCE.equals(dep.maintenanceLevel()))
            .count();

    return new DependencyMetrics(
        totalHealthScore,
        ageCounts.getOrDefault(FRESH_AGE, 0L),
        ageCounts.getOrDefault(CURRENT_AGE, 0L),
        ageCounts.getOrDefault(AGING_AGE, 0L),
        ageCounts.getOrDefault(STALE_AGE, 0L),
        activeMaintenanceCount);
  }

  private String determineOverallHealth(double averageHealthScore) {
    if (averageHealthScore >= EXCELLENT_HEALTH_THRESHOLD) {
      return EXCELLENT_HEALTH;
    } else if (averageHealthScore >= GOOD_HEALTH_THRESHOLD) {
      return GOOD_HEALTH;
    } else if (averageHealthScore >= FAIR_HEALTH_THRESHOLD) {
      return FAIR_HEALTH;
    } else {
      return POOR_HEALTH;
    }
  }

  private List<String> generateHealthRecommendations(
      DependencyMetrics metrics,
      int successfulAnalyses,
      List<ProjectHealthAnalysis.DependencyHealthAnalysis> successfulDeps,
      Integer maxAgeInDays) {
    List<String> recommendations = new ArrayList<>();

    if (metrics.staleCount() > 0) {
      recommendations.add(
          "Review " + metrics.staleCount() + " stale dependencies for alternatives");
    }
    if (metrics.agingCount() > successfulAnalyses / 2) {
      recommendations.add("Consider updating aging dependencies");
    }
    if (metrics.activeMaintenanceCount() < successfulAnalyses / 2) {
      recommendations.add("Monitor maintenance activity for slower-updated dependencies");
    }

    // Add age-specific recommendations when custom threshold is set
    if (maxAgeInDays != null) {
      long exceedsThreshold =
          successfulDeps.stream()
              .filter(
                  dep ->
                      STALE_AGE.equals(dep.ageClassification())
                          || AGING_AGE.equals(dep.ageClassification()))
              .count();
      if (exceedsThreshold > 0) {
        recommendations.add(
            "Found "
                + exceedsThreshold
                + " dependencies exceeding your "
                + maxAgeInDays
                + "-day age threshold");
      }
    }

    return recommendations;
  }

  private record DependencyMetrics(
      int totalHealthScore,
      long freshCount,
      long currentCount,
      long agingCount,
      long staleCount,
      long activeMaintenanceCount) {}
}

```
Page 2/2FirstPrevNextLast