Filter test_only artifacts out of artifacts merged into root repos and print a warning when a root artifact version is overridden by a non_root bazel_dep (#1511)

diff --git a/examples/bzlmod/maven_install.json b/examples/bzlmod/maven_install.json
index 23fc9e0..1357913 100644
--- a/examples/bzlmod/maven_install.json
+++ b/examples/bzlmod/maven_install.json
@@ -1,10 +1,7 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
-  "__INPUT_ARTIFACTS_HASH": -200671778,
-  "__RESOLVED_ARTIFACTS_HASH": -1275115130,
-  "conflict_resolution": {
-    "com.google.errorprone:error_prone_annotations:2.23.0": "com.google.errorprone:error_prone_annotations:2.24.1"
-  },
+  "__INPUT_ARTIFACTS_HASH": 1563368834,
+  "__RESOLVED_ARTIFACTS_HASH": -144767737,
   "artifacts": {
     "com.beust:jcommander": {
       "shasums": {
@@ -27,13 +24,6 @@
       },
       "version": "1.0.1"
     },
-    "com.google.auto.value:auto-value-annotations": {
-      "shasums": {
-        "jar": "e1c45e6beadaef9797cb0d9afd5a45621ad061cd8632012f85582853a3887825",
-        "sources": "61a433f015b12a6cf4ecff227c7748486ff8f294ffe9d39827b382ade0514d0a"
-      },
-      "version": "1.10.4"
-    },
     "com.google.auto:auto-common": {
       "shasums": {
         "jar": "326d674b411ea67505273f9ade5311c15bca50644b5211a6c309c9aee590a20a",
@@ -57,10 +47,10 @@
     },
     "com.google.errorprone:error_prone_annotations": {
       "shasums": {
-        "jar": "19fe2f7155d20ea093168527999da98108103ee546d1e8b726bc4b27c31a3c30",
-        "sources": "d0d63b507b92b36d38d938e96daf260baa55e739fe9e0d642519b8fac823d288"
+        "jar": "ec6f39f068b6ff9ac323c68e28b9299f8c0a80ca512dccb1d4a70f40ac3ec054",
+        "sources": "5b4504609bb93d3c24b87cd839cf0bb7d878135d0a917a05081d0dc9b2a9973f"
       },
-      "version": "2.24.1"
+      "version": "2.23.0"
     },
     "com.google.guava:failureaccess": {
       "shasums": {
@@ -103,13 +93,6 @@
       },
       "version": "4.27.2"
     },
-    "com.google.truth:truth": {
-      "shasums": {
-        "jar": "235c28e96ee6701ab01cc852fb294cb0f34756f636a8154b9aef08fb1215bbc4",
-        "sources": "f2834e7abdd08f5ab31defe409f601e0863b31f3f47105ab3c5ae8df4a687318"
-      },
-      "version": "1.4.0"
-    },
     "com.sun.activation:jakarta.activation": {
       "shasums": {
         "jar": "02156773e4ae9d048d14a56ad35d644bee9f1052a791d072df3ded3c656e6e1a",
@@ -313,26 +296,12 @@
       },
       "version": "2.0.0"
     },
-    "junit:junit": {
-      "shasums": {
-        "jar": "8e495b634469d64fb8acfa3495a065cbacc8a0fff55ce1e31007be4c16dc57d3",
-        "sources": "34181df6482d40ea4c046b063cb53c7ffae94bdf1b1d62695bdf3adf9dea7e3a"
-      },
-      "version": "4.13.2"
-    },
     "net.bytebuddy:byte-buddy": {
       "shasums": {
-        "jar": "e99761a526df0fefbbd3fe14436b0f953000cdfa5151dc63c0b18d37d9c46f1c",
-        "sources": "143dd9fe73f0566cc703934b7fd15abbb97bfab045064c2f176067e70456a136"
+        "jar": "1a1ac9ce65eddcea54ead958387bb0b3863d02a2ffe856ab6a57ac79737c19cf",
+        "sources": "6d2fc2af6c71077dc9fb097c8eb06023545a04b6839bcea0d8d388b1c5e65b61"
       },
-      "version": "1.14.5"
-    },
-    "net.bytebuddy:byte-buddy-agent": {
-      "shasums": {
-        "jar": "55f19862b870f5d85890ba5386b1b45e9bbc88d5fe1f819abe0c788b4929fa6b",
-        "sources": "8844d4b130218aa87e0f6b99f9a9b40971b45a95f44a0dfd409038c46631cb5b"
-      },
-      "version": "1.14.5"
+      "version": "1.12.10"
     },
     "org.apache.commons:commons-exec": {
       "shasums": {
@@ -357,38 +326,10 @@
     },
     "org.checkerframework:checker-qual": {
       "shasums": {
-        "jar": "ccaedd33af0b7894d9f2f3b644f4d19e43928e32902e61ac4d10777830f5aac7",
-        "sources": "efb65eb479f61f53c6dcafbd42ed59dad09b0a0d5a7f44b7bc68df25c2dcf8fd"
+        "jar": "2f9f245bf68e4259d610894f2406dc1f6363dc639302bd566e8272e4f4541172",
+        "sources": "8308220bbdd4e12b49fa06a91de685faf9cc1a376464478c80845be3e87b7d4f"
       },
-      "version": "3.42.0"
-    },
-    "org.hamcrest:hamcrest-core": {
-      "shasums": {
-        "jar": "66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9",
-        "sources": "e223d2d8fbafd66057a8848cc94222d63c3cedd652cc48eddc0ab5c39c0f84df"
-      },
-      "version": "1.3"
-    },
-    "org.mockito:mockito-core": {
-      "shasums": {
-        "jar": "b1689b06617ea01fd777bfaedbdde512faf083d639a049f79b388d5a4e96d2e5",
-        "sources": "f21eb1cbb701477ba37c430b5a97ba78e3b3cf79a5737443e06b3f65bd7c9364"
-      },
-      "version": "5.4.0"
-    },
-    "org.objenesis:objenesis": {
-      "shasums": {
-        "jar": "02dfd0b0439a5591e35b708ed2f5474eb0948f53abf74637e959b8e4ef69bfeb",
-        "sources": "d06164f8ca002c8ef193cef2d682822014dd330505616af93a3fb64226fc131d"
-      },
-      "version": "3.3"
-    },
-    "org.ow2.asm:asm": {
-      "shasums": {
-        "jar": "3c6fac2424db3d4a853b669f4e3d1d9c3c552235e19a319673f887083c2303a1",
-        "sources": "2b6e12f0da3d065ba628a024a8851ab0d5b5d3501dacfcc18769243250f4f77e"
-      },
-      "version": "9.6"
+      "version": "3.41.0"
     },
     "org.reactivestreams:reactive-streams": {
       "shasums": {
@@ -548,14 +489,6 @@
       "com.google.j2objc:j2objc-annotations",
       "com.google.protobuf:protobuf-java"
     ],
-    "com.google.truth:truth": [
-      "com.google.auto.value:auto-value-annotations",
-      "com.google.errorprone:error_prone_annotations",
-      "com.google.guava:guava",
-      "junit:junit",
-      "org.checkerframework:checker-qual",
-      "org.ow2.asm:asm"
-    ],
     "com.typesafe.netty:netty-reactive-streams": [
       "io.netty:netty-handler",
       "org.reactivestreams:reactive-streams"
@@ -682,9 +615,6 @@
     "io.opentelemetry:opentelemetry-semconv": [
       "io.opentelemetry:opentelemetry-api"
     ],
-    "junit:junit": [
-      "org.hamcrest:hamcrest-core"
-    ],
     "org.asynchttpclient:async-http-client": [
       "com.sun.activation:jakarta.activation",
       "com.typesafe.netty:netty-reactive-streams",
@@ -701,11 +631,6 @@
       "io.netty:netty-buffer",
       "org.slf4j:slf4j-api"
     ],
-    "org.mockito:mockito-core": [
-      "net.bytebuddy:byte-buddy",
-      "net.bytebuddy:byte-buddy-agent",
-      "org.objenesis:objenesis"
-    ],
     "org.seleniumhq.selenium:selenium-chrome-driver": [
       "com.google.auto.service:auto-service",
       "com.google.auto.service:auto-service-annotations",
@@ -871,12 +796,6 @@
     "com.google.auto.service:auto-service-annotations": [
       "com.google.auto.service"
     ],
-    "com.google.auto.value:auto-value-annotations": [
-      "com.google.auto.value",
-      "com.google.auto.value.extension.memoized",
-      "com.google.auto.value.extension.serializable",
-      "com.google.auto.value.extension.toprettystring"
-    ],
     "com.google.auto:auto-common": [
       "com.google.auto.common"
     ],
@@ -933,9 +852,6 @@
     "com.google.protobuf:protobuf-java-util": [
       "com.google.protobuf.util"
     ],
-    "com.google.truth:truth": [
-      "com.google.common.truth"
-    ],
     "com.sun.activation:jakarta.activation": [
       "com.sun.activation.registries",
       "com.sun.activation.viewers",
@@ -1118,40 +1034,6 @@
       "io.ous.jtoml",
       "io.ous.jtoml.impl"
     ],
-    "junit:junit": [
-      "junit.extensions",
-      "junit.framework",
-      "junit.runner",
-      "junit.textui",
-      "org.junit",
-      "org.junit.experimental",
-      "org.junit.experimental.categories",
-      "org.junit.experimental.max",
-      "org.junit.experimental.results",
-      "org.junit.experimental.runners",
-      "org.junit.experimental.theories",
-      "org.junit.experimental.theories.internal",
-      "org.junit.experimental.theories.suppliers",
-      "org.junit.function",
-      "org.junit.internal",
-      "org.junit.internal.builders",
-      "org.junit.internal.management",
-      "org.junit.internal.matchers",
-      "org.junit.internal.requests",
-      "org.junit.internal.runners",
-      "org.junit.internal.runners.model",
-      "org.junit.internal.runners.rules",
-      "org.junit.internal.runners.statements",
-      "org.junit.matchers",
-      "org.junit.rules",
-      "org.junit.runner",
-      "org.junit.runner.manipulation",
-      "org.junit.runner.notification",
-      "org.junit.runners",
-      "org.junit.runners.model",
-      "org.junit.runners.parameterized",
-      "org.junit.validator"
-    ],
     "net.bytebuddy:byte-buddy": [
       "net.bytebuddy",
       "net.bytebuddy.agent.builder",
@@ -1192,10 +1074,6 @@
       "net.bytebuddy.utility.privilege",
       "net.bytebuddy.utility.visitor"
     ],
-    "net.bytebuddy:byte-buddy-agent": [
-      "net.bytebuddy.agent",
-      "net.bytebuddy.agent.utility.nullability"
-    ],
     "net.bytebuddy:byte-buddy:jar:sources": [
       "net.bytebuddy.build"
     ],
@@ -1268,98 +1146,11 @@
       "org.checkerframework.common.reflection.qual",
       "org.checkerframework.common.returnsreceiver.qual",
       "org.checkerframework.common.subtyping.qual",
-      "org.checkerframework.common.util.count.report.qual",
+      "org.checkerframework.common.util.report.qual",
       "org.checkerframework.common.value.qual",
       "org.checkerframework.dataflow.qual",
       "org.checkerframework.framework.qual"
     ],
-    "org.hamcrest:hamcrest-core": [
-      "org.hamcrest",
-      "org.hamcrest.core",
-      "org.hamcrest.internal"
-    ],
-    "org.mockito:mockito-core": [
-      "org.mockito",
-      "org.mockito.codegen",
-      "org.mockito.configuration",
-      "org.mockito.creation.instance",
-      "org.mockito.exceptions.base",
-      "org.mockito.exceptions.misusing",
-      "org.mockito.exceptions.stacktrace",
-      "org.mockito.exceptions.verification",
-      "org.mockito.exceptions.verification.junit",
-      "org.mockito.exceptions.verification.opentest4j",
-      "org.mockito.hamcrest",
-      "org.mockito.internal",
-      "org.mockito.internal.configuration",
-      "org.mockito.internal.configuration.injection",
-      "org.mockito.internal.configuration.injection.filter",
-      "org.mockito.internal.configuration.injection.scanner",
-      "org.mockito.internal.configuration.plugins",
-      "org.mockito.internal.creation",
-      "org.mockito.internal.creation.bytebuddy",
-      "org.mockito.internal.creation.instance",
-      "org.mockito.internal.creation.proxy",
-      "org.mockito.internal.creation.settings",
-      "org.mockito.internal.creation.util",
-      "org.mockito.internal.debugging",
-      "org.mockito.internal.exceptions",
-      "org.mockito.internal.exceptions.stacktrace",
-      "org.mockito.internal.exceptions.util",
-      "org.mockito.internal.framework",
-      "org.mockito.internal.hamcrest",
-      "org.mockito.internal.handler",
-      "org.mockito.internal.invocation",
-      "org.mockito.internal.invocation.finder",
-      "org.mockito.internal.invocation.mockref",
-      "org.mockito.internal.junit",
-      "org.mockito.internal.listeners",
-      "org.mockito.internal.matchers",
-      "org.mockito.internal.matchers.apachecommons",
-      "org.mockito.internal.matchers.text",
-      "org.mockito.internal.progress",
-      "org.mockito.internal.reporting",
-      "org.mockito.internal.runners",
-      "org.mockito.internal.runners.util",
-      "org.mockito.internal.session",
-      "org.mockito.internal.stubbing",
-      "org.mockito.internal.stubbing.answers",
-      "org.mockito.internal.stubbing.defaultanswers",
-      "org.mockito.internal.util",
-      "org.mockito.internal.util.collections",
-      "org.mockito.internal.util.concurrent",
-      "org.mockito.internal.util.io",
-      "org.mockito.internal.util.reflection",
-      "org.mockito.internal.verification",
-      "org.mockito.internal.verification.api",
-      "org.mockito.internal.verification.argumentmatching",
-      "org.mockito.internal.verification.checkers",
-      "org.mockito.invocation",
-      "org.mockito.junit",
-      "org.mockito.listeners",
-      "org.mockito.mock",
-      "org.mockito.plugins",
-      "org.mockito.quality",
-      "org.mockito.session",
-      "org.mockito.stubbing",
-      "org.mockito.verification"
-    ],
-    "org.objenesis:objenesis": [
-      "org.objenesis",
-      "org.objenesis.instantiator",
-      "org.objenesis.instantiator.android",
-      "org.objenesis.instantiator.annotations",
-      "org.objenesis.instantiator.basic",
-      "org.objenesis.instantiator.gcj",
-      "org.objenesis.instantiator.perc",
-      "org.objenesis.instantiator.sun",
-      "org.objenesis.instantiator.util",
-      "org.objenesis.strategy"
-    ],
-    "org.ow2.asm:asm": [
-      "org.objectweb.asm",
-      "org.objectweb.asm.signature"
-    ],
     "org.reactivestreams:reactive-streams": [
       "org.reactivestreams"
     ],
@@ -1825,8 +1616,6 @@
       "com.google.auto.service:auto-service-annotations",
       "com.google.auto.service:auto-service-annotations:jar:sources",
       "com.google.auto.service:auto-service:jar:sources",
-      "com.google.auto.value:auto-value-annotations",
-      "com.google.auto.value:auto-value-annotations:jar:sources",
       "com.google.auto:auto-common",
       "com.google.auto:auto-common:jar:sources",
       "com.google.code.findbugs:jsr305",
@@ -1846,8 +1635,6 @@
       "com.google.protobuf:protobuf-java-util",
       "com.google.protobuf:protobuf-java-util:jar:sources",
       "com.google.protobuf:protobuf-java:jar:sources",
-      "com.google.truth:truth",
-      "com.google.truth:truth:jar:sources",
       "com.sun.activation:jakarta.activation",
       "com.sun.activation:jakarta.activation:jar:sources",
       "com.typesafe.netty:netty-reactive-streams",
@@ -1906,11 +1693,7 @@
       "io.opentelemetry:opentelemetry-semconv:jar:sources",
       "io.ous:jtoml",
       "io.ous:jtoml:jar:sources",
-      "junit:junit",
-      "junit:junit:jar:sources",
       "net.bytebuddy:byte-buddy",
-      "net.bytebuddy:byte-buddy-agent",
-      "net.bytebuddy:byte-buddy-agent:jar:sources",
       "net.bytebuddy:byte-buddy:jar:sources",
       "org.apache.commons:commons-exec",
       "org.apache.commons:commons-exec:jar:sources",
@@ -1920,14 +1703,6 @@
       "org.asynchttpclient:async-http-client:jar:sources",
       "org.checkerframework:checker-qual",
       "org.checkerframework:checker-qual:jar:sources",
-      "org.hamcrest:hamcrest-core",
-      "org.hamcrest:hamcrest-core:jar:sources",
-      "org.mockito:mockito-core",
-      "org.mockito:mockito-core:jar:sources",
-      "org.objenesis:objenesis",
-      "org.objenesis:objenesis:jar:sources",
-      "org.ow2.asm:asm",
-      "org.ow2.asm:asm:jar:sources",
       "org.reactivestreams:reactive-streams",
       "org.reactivestreams:reactive-streams:jar:sources",
       "org.seleniumhq.selenium:selenium-api",
@@ -2120,8 +1895,5 @@
       ]
     }
   },
-  "skipped": [
-    "com.google.guava:listenablefuture:jar:sources"
-  ],
   "version": "2"
 }
diff --git a/examples/java-export/maven_install.json b/examples/java-export/maven_install.json
index 2baabca..548bb99 100644
--- a/examples/java-export/maven_install.json
+++ b/examples/java-export/maven_install.json
@@ -1,20 +1,11 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
-  "__INPUT_ARTIFACTS_HASH": -1781511218,
-  "__RESOLVED_ARTIFACTS_HASH": 1370831359,
+  "__INPUT_ARTIFACTS_HASH": 908270220,
+  "__RESOLVED_ARTIFACTS_HASH": -8035072,
   "conflict_resolution": {
-    "com.google.errorprone:error_prone_annotations:2.23.0": "com.google.errorprone:error_prone_annotations:2.24.1",
-    "com.google.guava:guava:29.0-jre": "com.google.guava:guava:33.0.0-jre",
-    "com.google.protobuf:protobuf-java:4.27.2": "com.google.protobuf:protobuf-java:4.31.1"
+    "com.google.guava:guava:29.0-jre": "com.google.guava:guava:33.0.0-jre"
   },
   "artifacts": {
-    "com.google.auto.value:auto-value-annotations": {
-      "shasums": {
-        "jar": "e1c45e6beadaef9797cb0d9afd5a45621ad061cd8632012f85582853a3887825",
-        "sources": "61a433f015b12a6cf4ecff227c7748486ff8f294ffe9d39827b382ade0514d0a"
-      },
-      "version": "1.10.4"
-    },
     "com.google.code.findbugs:jsr305": {
       "shasums": {
         "jar": "766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7",
@@ -31,10 +22,10 @@
     },
     "com.google.errorprone:error_prone_annotations": {
       "shasums": {
-        "jar": "19fe2f7155d20ea093168527999da98108103ee546d1e8b726bc4b27c31a3c30",
-        "sources": "d0d63b507b92b36d38d938e96daf260baa55e739fe9e0d642519b8fac823d288"
+        "jar": "ec6f39f068b6ff9ac323c68e28b9299f8c0a80ca512dccb1d4a70f40ac3ec054",
+        "sources": "5b4504609bb93d3c24b87cd839cf0bb7d878135d0a917a05081d0dc9b2a9973f"
       },
-      "version": "2.24.1"
+      "version": "2.23.0"
     },
     "com.google.guava:failureaccess": {
       "shasums": {
@@ -77,68 +68,12 @@
       },
       "version": "4.27.2"
     },
-    "com.google.truth:truth": {
-      "shasums": {
-        "jar": "235c28e96ee6701ab01cc852fb294cb0f34756f636a8154b9aef08fb1215bbc4",
-        "sources": "f2834e7abdd08f5ab31defe409f601e0863b31f3f47105ab3c5ae8df4a687318"
-      },
-      "version": "1.4.0"
-    },
-    "junit:junit": {
-      "shasums": {
-        "jar": "8e495b634469d64fb8acfa3495a065cbacc8a0fff55ce1e31007be4c16dc57d3",
-        "sources": "34181df6482d40ea4c046b063cb53c7ffae94bdf1b1d62695bdf3adf9dea7e3a"
-      },
-      "version": "4.13.2"
-    },
-    "net.bytebuddy:byte-buddy": {
-      "shasums": {
-        "jar": "e99761a526df0fefbbd3fe14436b0f953000cdfa5151dc63c0b18d37d9c46f1c",
-        "sources": "143dd9fe73f0566cc703934b7fd15abbb97bfab045064c2f176067e70456a136"
-      },
-      "version": "1.14.5"
-    },
-    "net.bytebuddy:byte-buddy-agent": {
-      "shasums": {
-        "jar": "55f19862b870f5d85890ba5386b1b45e9bbc88d5fe1f819abe0c788b4929fa6b",
-        "sources": "8844d4b130218aa87e0f6b99f9a9b40971b45a95f44a0dfd409038c46631cb5b"
-      },
-      "version": "1.14.5"
-    },
     "org.checkerframework:checker-qual": {
       "shasums": {
-        "jar": "ccaedd33af0b7894d9f2f3b644f4d19e43928e32902e61ac4d10777830f5aac7",
-        "sources": "efb65eb479f61f53c6dcafbd42ed59dad09b0a0d5a7f44b7bc68df25c2dcf8fd"
+        "jar": "2f9f245bf68e4259d610894f2406dc1f6363dc639302bd566e8272e4f4541172",
+        "sources": "8308220bbdd4e12b49fa06a91de685faf9cc1a376464478c80845be3e87b7d4f"
       },
-      "version": "3.42.0"
-    },
-    "org.hamcrest:hamcrest-core": {
-      "shasums": {
-        "jar": "66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9",
-        "sources": "e223d2d8fbafd66057a8848cc94222d63c3cedd652cc48eddc0ab5c39c0f84df"
-      },
-      "version": "1.3"
-    },
-    "org.mockito:mockito-core": {
-      "shasums": {
-        "jar": "b1689b06617ea01fd777bfaedbdde512faf083d639a049f79b388d5a4e96d2e5",
-        "sources": "f21eb1cbb701477ba37c430b5a97ba78e3b3cf79a5737443e06b3f65bd7c9364"
-      },
-      "version": "5.4.0"
-    },
-    "org.objenesis:objenesis": {
-      "shasums": {
-        "jar": "02dfd0b0439a5591e35b708ed2f5474eb0948f53abf74637e959b8e4ef69bfeb",
-        "sources": "d06164f8ca002c8ef193cef2d682822014dd330505616af93a3fb64226fc131d"
-      },
-      "version": "3.3"
-    },
-    "org.ow2.asm:asm": {
-      "shasums": {
-        "jar": "3c6fac2424db3d4a853b669f4e3d1d9c3c552235e19a319673f887083c2303a1",
-        "sources": "2b6e12f0da3d065ba628a024a8851ab0d5b5d3501dacfcc18769243250f4f77e"
-      },
-      "version": "9.6"
+      "version": "3.41.0"
     }
   },
   "dependencies": {
@@ -157,31 +92,9 @@
       "com.google.guava:guava",
       "com.google.j2objc:j2objc-annotations",
       "com.google.protobuf:protobuf-java"
-    ],
-    "com.google.truth:truth": [
-      "com.google.auto.value:auto-value-annotations",
-      "com.google.errorprone:error_prone_annotations",
-      "com.google.guava:guava",
-      "junit:junit",
-      "org.checkerframework:checker-qual",
-      "org.ow2.asm:asm"
-    ],
-    "junit:junit": [
-      "org.hamcrest:hamcrest-core"
-    ],
-    "org.mockito:mockito-core": [
-      "net.bytebuddy:byte-buddy",
-      "net.bytebuddy:byte-buddy-agent",
-      "org.objenesis:objenesis"
     ]
   },
   "packages": {
-    "com.google.auto.value:auto-value-annotations": [
-      "com.google.auto.value",
-      "com.google.auto.value.extension.memoized",
-      "com.google.auto.value.extension.serializable",
-      "com.google.auto.value.extension.toprettystring"
-    ],
     "com.google.code.findbugs:jsr305": [
       "javax.annotation",
       "javax.annotation.concurrent",
@@ -235,90 +148,6 @@
     "com.google.protobuf:protobuf-java-util": [
       "com.google.protobuf.util"
     ],
-    "com.google.truth:truth": [
-      "com.google.common.truth"
-    ],
-    "junit:junit": [
-      "junit.extensions",
-      "junit.framework",
-      "junit.runner",
-      "junit.textui",
-      "org.junit",
-      "org.junit.experimental",
-      "org.junit.experimental.categories",
-      "org.junit.experimental.max",
-      "org.junit.experimental.results",
-      "org.junit.experimental.runners",
-      "org.junit.experimental.theories",
-      "org.junit.experimental.theories.internal",
-      "org.junit.experimental.theories.suppliers",
-      "org.junit.function",
-      "org.junit.internal",
-      "org.junit.internal.builders",
-      "org.junit.internal.management",
-      "org.junit.internal.matchers",
-      "org.junit.internal.requests",
-      "org.junit.internal.runners",
-      "org.junit.internal.runners.model",
-      "org.junit.internal.runners.rules",
-      "org.junit.internal.runners.statements",
-      "org.junit.matchers",
-      "org.junit.rules",
-      "org.junit.runner",
-      "org.junit.runner.manipulation",
-      "org.junit.runner.notification",
-      "org.junit.runners",
-      "org.junit.runners.model",
-      "org.junit.runners.parameterized",
-      "org.junit.validator"
-    ],
-    "net.bytebuddy:byte-buddy": [
-      "net.bytebuddy",
-      "net.bytebuddy.agent.builder",
-      "net.bytebuddy.asm",
-      "net.bytebuddy.build",
-      "net.bytebuddy.description",
-      "net.bytebuddy.description.annotation",
-      "net.bytebuddy.description.enumeration",
-      "net.bytebuddy.description.field",
-      "net.bytebuddy.description.method",
-      "net.bytebuddy.description.modifier",
-      "net.bytebuddy.description.type",
-      "net.bytebuddy.dynamic",
-      "net.bytebuddy.dynamic.loading",
-      "net.bytebuddy.dynamic.scaffold",
-      "net.bytebuddy.dynamic.scaffold.inline",
-      "net.bytebuddy.dynamic.scaffold.subclass",
-      "net.bytebuddy.implementation",
-      "net.bytebuddy.implementation.attribute",
-      "net.bytebuddy.implementation.auxiliary",
-      "net.bytebuddy.implementation.bind",
-      "net.bytebuddy.implementation.bind.annotation",
-      "net.bytebuddy.implementation.bytecode",
-      "net.bytebuddy.implementation.bytecode.assign",
-      "net.bytebuddy.implementation.bytecode.assign.primitive",
-      "net.bytebuddy.implementation.bytecode.assign.reference",
-      "net.bytebuddy.implementation.bytecode.collection",
-      "net.bytebuddy.implementation.bytecode.constant",
-      "net.bytebuddy.implementation.bytecode.member",
-      "net.bytebuddy.jar.asm",
-      "net.bytebuddy.jar.asm.commons",
-      "net.bytebuddy.jar.asm.signature",
-      "net.bytebuddy.matcher",
-      "net.bytebuddy.pool",
-      "net.bytebuddy.utility",
-      "net.bytebuddy.utility.dispatcher",
-      "net.bytebuddy.utility.nullability",
-      "net.bytebuddy.utility.privilege",
-      "net.bytebuddy.utility.visitor"
-    ],
-    "net.bytebuddy:byte-buddy-agent": [
-      "net.bytebuddy.agent",
-      "net.bytebuddy.agent.utility.nullability"
-    ],
-    "net.bytebuddy:byte-buddy:jar:sources": [
-      "net.bytebuddy.build"
-    ],
     "org.checkerframework:checker-qual": [
       "org.checkerframework.checker.builder.qual",
       "org.checkerframework.checker.calledmethods.qual",
@@ -346,103 +175,14 @@
       "org.checkerframework.common.reflection.qual",
       "org.checkerframework.common.returnsreceiver.qual",
       "org.checkerframework.common.subtyping.qual",
-      "org.checkerframework.common.util.count.report.qual",
+      "org.checkerframework.common.util.report.qual",
       "org.checkerframework.common.value.qual",
       "org.checkerframework.dataflow.qual",
       "org.checkerframework.framework.qual"
-    ],
-    "org.hamcrest:hamcrest-core": [
-      "org.hamcrest",
-      "org.hamcrest.core",
-      "org.hamcrest.internal"
-    ],
-    "org.mockito:mockito-core": [
-      "org.mockito",
-      "org.mockito.codegen",
-      "org.mockito.configuration",
-      "org.mockito.creation.instance",
-      "org.mockito.exceptions.base",
-      "org.mockito.exceptions.misusing",
-      "org.mockito.exceptions.stacktrace",
-      "org.mockito.exceptions.verification",
-      "org.mockito.exceptions.verification.junit",
-      "org.mockito.exceptions.verification.opentest4j",
-      "org.mockito.hamcrest",
-      "org.mockito.internal",
-      "org.mockito.internal.configuration",
-      "org.mockito.internal.configuration.injection",
-      "org.mockito.internal.configuration.injection.filter",
-      "org.mockito.internal.configuration.injection.scanner",
-      "org.mockito.internal.configuration.plugins",
-      "org.mockito.internal.creation",
-      "org.mockito.internal.creation.bytebuddy",
-      "org.mockito.internal.creation.instance",
-      "org.mockito.internal.creation.proxy",
-      "org.mockito.internal.creation.settings",
-      "org.mockito.internal.creation.util",
-      "org.mockito.internal.debugging",
-      "org.mockito.internal.exceptions",
-      "org.mockito.internal.exceptions.stacktrace",
-      "org.mockito.internal.exceptions.util",
-      "org.mockito.internal.framework",
-      "org.mockito.internal.hamcrest",
-      "org.mockito.internal.handler",
-      "org.mockito.internal.invocation",
-      "org.mockito.internal.invocation.finder",
-      "org.mockito.internal.invocation.mockref",
-      "org.mockito.internal.junit",
-      "org.mockito.internal.listeners",
-      "org.mockito.internal.matchers",
-      "org.mockito.internal.matchers.apachecommons",
-      "org.mockito.internal.matchers.text",
-      "org.mockito.internal.progress",
-      "org.mockito.internal.reporting",
-      "org.mockito.internal.runners",
-      "org.mockito.internal.runners.util",
-      "org.mockito.internal.session",
-      "org.mockito.internal.stubbing",
-      "org.mockito.internal.stubbing.answers",
-      "org.mockito.internal.stubbing.defaultanswers",
-      "org.mockito.internal.util",
-      "org.mockito.internal.util.collections",
-      "org.mockito.internal.util.concurrent",
-      "org.mockito.internal.util.io",
-      "org.mockito.internal.util.reflection",
-      "org.mockito.internal.verification",
-      "org.mockito.internal.verification.api",
-      "org.mockito.internal.verification.argumentmatching",
-      "org.mockito.internal.verification.checkers",
-      "org.mockito.invocation",
-      "org.mockito.junit",
-      "org.mockito.listeners",
-      "org.mockito.mock",
-      "org.mockito.plugins",
-      "org.mockito.quality",
-      "org.mockito.session",
-      "org.mockito.stubbing",
-      "org.mockito.verification"
-    ],
-    "org.objenesis:objenesis": [
-      "org.objenesis",
-      "org.objenesis.instantiator",
-      "org.objenesis.instantiator.android",
-      "org.objenesis.instantiator.annotations",
-      "org.objenesis.instantiator.basic",
-      "org.objenesis.instantiator.gcj",
-      "org.objenesis.instantiator.perc",
-      "org.objenesis.instantiator.sun",
-      "org.objenesis.instantiator.util",
-      "org.objenesis.strategy"
-    ],
-    "org.ow2.asm:asm": [
-      "org.objectweb.asm",
-      "org.objectweb.asm.signature"
     ]
   },
   "repositories": {
     "https://repo1.maven.org/maven2/": [
-      "com.google.auto.value:auto-value-annotations",
-      "com.google.auto.value:auto-value-annotations:jar:sources",
       "com.google.code.findbugs:jsr305",
       "com.google.code.findbugs:jsr305:jar:sources",
       "com.google.code.gson:gson",
@@ -460,29 +200,10 @@
       "com.google.protobuf:protobuf-java-util",
       "com.google.protobuf:protobuf-java-util:jar:sources",
       "com.google.protobuf:protobuf-java:jar:sources",
-      "com.google.truth:truth",
-      "com.google.truth:truth:jar:sources",
-      "junit:junit",
-      "junit:junit:jar:sources",
-      "net.bytebuddy:byte-buddy",
-      "net.bytebuddy:byte-buddy-agent",
-      "net.bytebuddy:byte-buddy-agent:jar:sources",
-      "net.bytebuddy:byte-buddy:jar:sources",
       "org.checkerframework:checker-qual",
-      "org.checkerframework:checker-qual:jar:sources",
-      "org.hamcrest:hamcrest-core",
-      "org.hamcrest:hamcrest-core:jar:sources",
-      "org.mockito:mockito-core",
-      "org.mockito:mockito-core:jar:sources",
-      "org.objenesis:objenesis",
-      "org.objenesis:objenesis:jar:sources",
-      "org.ow2.asm:asm",
-      "org.ow2.asm:asm:jar:sources"
+      "org.checkerframework:checker-qual:jar:sources"
     ]
   },
   "services": {},
-  "skipped": [
-    "com.google.guava:listenablefuture:jar:sources"
-  ],
   "version": "2"
 }
diff --git a/maven_install.json b/maven_install.json
index 07c3b81..8042b3c 100644
--- a/maven_install.json
+++ b/maven_install.json
@@ -1,19 +1,8 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
-  "__INPUT_ARTIFACTS_HASH": 1994476565,
-  "__RESOLVED_ARTIFACTS_HASH": -274973469,
-  "conflict_resolution": {
-    "com.google.errorprone:error_prone_annotations:2.23.0": "com.google.errorprone:error_prone_annotations:2.41.0",
-    "com.google.guava:guava:33.0.0-jre": "com.google.guava:guava:33.5.0-jre",
-    "com.google.protobuf:protobuf-java:4.27.2": "com.google.protobuf:protobuf-java:4.31.0"
-  },
+  "__INPUT_ARTIFACTS_HASH": -647997795,
+  "__RESOLVED_ARTIFACTS_HASH": 1828317327,
   "artifacts": {
-    "com.google.auto.value:auto-value-annotations": {
-      "shasums": {
-        "jar": "e1c45e6beadaef9797cb0d9afd5a45621ad061cd8632012f85582853a3887825"
-      },
-      "version": "1.10.4"
-    },
     "com.google.code.findbugs:jsr305": {
       "shasums": {
         "jar": "766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7"
@@ -28,9 +17,9 @@
     },
     "com.google.errorprone:error_prone_annotations": {
       "shasums": {
-        "jar": "a56e782b5b50811ac204073a355a21d915a2107fce13ec711331ad036f660fcc"
+        "jar": "ec6f39f068b6ff9ac323c68e28b9299f8c0a80ca512dccb1d4a70f40ac3ec054"
       },
-      "version": "2.41.0"
+      "version": "2.23.0"
     },
     "com.google.guava:failureaccess": {
       "shasums": {
@@ -52,9 +41,9 @@
     },
     "com.google.j2objc:j2objc-annotations": {
       "shasums": {
-        "jar": "84d3a150518485f8140ea99b8a985656749629f6433c92b80c75b36aba3b099b"
+        "jar": "f02a95fa1a5e95edb3ed859fd0fb7df709d121a35290eff8b74dce2ab7f4d6ed"
       },
-      "version": "3.1"
+      "version": "2.8"
     },
     "com.google.protobuf:protobuf-java": {
       "shasums": {
@@ -68,12 +57,6 @@
       },
       "version": "4.27.2"
     },
-    "com.google.truth:truth": {
-      "shasums": {
-        "jar": "235c28e96ee6701ab01cc852fb294cb0f34756f636a8154b9aef08fb1215bbc4"
-      },
-      "version": "1.4.0"
-    },
     "io.netty:netty-tcnative-boringssl-static": {
       "shasums": {
         "jar": "b6f974972c44cd6f9cecabc255290286faac40b6393c66c3c3c0db7f421cc28e",
@@ -91,30 +74,6 @@
       },
       "version": "2.0.61.Final"
     },
-    "junit:junit": {
-      "shasums": {
-        "jar": "8e495b634469d64fb8acfa3495a065cbacc8a0fff55ce1e31007be4c16dc57d3"
-      },
-      "version": "4.13.2"
-    },
-    "net.bytebuddy:byte-buddy": {
-      "shasums": {
-        "jar": "e99761a526df0fefbbd3fe14436b0f953000cdfa5151dc63c0b18d37d9c46f1c"
-      },
-      "version": "1.14.5"
-    },
-    "net.bytebuddy:byte-buddy-agent": {
-      "shasums": {
-        "jar": "55f19862b870f5d85890ba5386b1b45e9bbc88d5fe1f819abe0c788b4929fa6b"
-      },
-      "version": "1.14.5"
-    },
-    "org.checkerframework:checker-qual": {
-      "shasums": {
-        "jar": "ccaedd33af0b7894d9f2f3b644f4d19e43928e32902e61ac4d10777830f5aac7"
-      },
-      "version": "3.42.0"
-    },
     "org.hamcrest:hamcrest": {
       "shasums": {
         "jar": "ba93b2e3a562322ba432f0a1b53addcc55cb188253319a020ed77f824e692050"
@@ -132,24 +91,6 @@
         "jar": "1fad6e6be7557781e4d33729d49ae1cdc8fdda6fe477bb0cc68ce351eafdfbab"
       },
       "version": "1.0.0"
-    },
-    "org.mockito:mockito-core": {
-      "shasums": {
-        "jar": "b1689b06617ea01fd777bfaedbdde512faf083d639a049f79b388d5a4e96d2e5"
-      },
-      "version": "5.4.0"
-    },
-    "org.objenesis:objenesis": {
-      "shasums": {
-        "jar": "02dfd0b0439a5591e35b708ed2f5474eb0948f53abf74637e959b8e4ef69bfeb"
-      },
-      "version": "3.3"
-    },
-    "org.ow2.asm:asm": {
-      "shasums": {
-        "jar": "3c6fac2424db3d4a853b669f4e3d1d9c3c552235e19a319673f887083c2303a1"
-      },
-      "version": "9.6"
     }
   },
   "dependencies": {
@@ -168,14 +109,6 @@
       "com.google.j2objc:j2objc-annotations",
       "com.google.protobuf:protobuf-java"
     ],
-    "com.google.truth:truth": [
-      "com.google.auto.value:auto-value-annotations",
-      "com.google.errorprone:error_prone_annotations",
-      "com.google.guava:guava",
-      "junit:junit",
-      "org.checkerframework:checker-qual",
-      "org.ow2.asm:asm"
-    ],
     "io.netty:netty-tcnative-boringssl-static": [
       "io.netty:netty-tcnative-boringssl-static:jar:linux-aarch_64",
       "io.netty:netty-tcnative-boringssl-static:jar:linux-x86_64",
@@ -219,25 +152,11 @@
       "io.netty:netty-tcnative-boringssl-static:jar:osx-x86_64",
       "io.netty:netty-tcnative-classes"
     ],
-    "junit:junit": [
-      "org.hamcrest:hamcrest-core"
-    ],
     "org.hamcrest:hamcrest-core": [
       "org.hamcrest:hamcrest"
-    ],
-    "org.mockito:mockito-core": [
-      "net.bytebuddy:byte-buddy",
-      "net.bytebuddy:byte-buddy-agent",
-      "org.objenesis:objenesis"
     ]
   },
   "packages": {
-    "com.google.auto.value:auto-value-annotations": [
-      "com.google.auto.value",
-      "com.google.auto.value.extension.memoized",
-      "com.google.auto.value.extension.serializable",
-      "com.google.auto.value.extension.toprettystring"
-    ],
     "com.google.code.findbugs:jsr305": [
       "javax.annotation",
       "javax.annotation.concurrent",
@@ -291,122 +210,9 @@
     "com.google.protobuf:protobuf-java-util": [
       "com.google.protobuf.util"
     ],
-    "com.google.truth:truth": [
-      "com.google.common.truth"
-    ],
     "io.netty:netty-tcnative-classes": [
       "io.netty.internal.tcnative"
     ],
-    "junit:junit": [
-      "junit.extensions",
-      "junit.framework",
-      "junit.runner",
-      "junit.textui",
-      "org.junit",
-      "org.junit.experimental",
-      "org.junit.experimental.categories",
-      "org.junit.experimental.max",
-      "org.junit.experimental.results",
-      "org.junit.experimental.runners",
-      "org.junit.experimental.theories",
-      "org.junit.experimental.theories.internal",
-      "org.junit.experimental.theories.suppliers",
-      "org.junit.function",
-      "org.junit.internal",
-      "org.junit.internal.builders",
-      "org.junit.internal.management",
-      "org.junit.internal.matchers",
-      "org.junit.internal.requests",
-      "org.junit.internal.runners",
-      "org.junit.internal.runners.model",
-      "org.junit.internal.runners.rules",
-      "org.junit.internal.runners.statements",
-      "org.junit.matchers",
-      "org.junit.rules",
-      "org.junit.runner",
-      "org.junit.runner.manipulation",
-      "org.junit.runner.notification",
-      "org.junit.runners",
-      "org.junit.runners.model",
-      "org.junit.runners.parameterized",
-      "org.junit.validator"
-    ],
-    "net.bytebuddy:byte-buddy": [
-      "net.bytebuddy",
-      "net.bytebuddy.agent.builder",
-      "net.bytebuddy.asm",
-      "net.bytebuddy.build",
-      "net.bytebuddy.description",
-      "net.bytebuddy.description.annotation",
-      "net.bytebuddy.description.enumeration",
-      "net.bytebuddy.description.field",
-      "net.bytebuddy.description.method",
-      "net.bytebuddy.description.modifier",
-      "net.bytebuddy.description.type",
-      "net.bytebuddy.dynamic",
-      "net.bytebuddy.dynamic.loading",
-      "net.bytebuddy.dynamic.scaffold",
-      "net.bytebuddy.dynamic.scaffold.inline",
-      "net.bytebuddy.dynamic.scaffold.subclass",
-      "net.bytebuddy.implementation",
-      "net.bytebuddy.implementation.attribute",
-      "net.bytebuddy.implementation.auxiliary",
-      "net.bytebuddy.implementation.bind",
-      "net.bytebuddy.implementation.bind.annotation",
-      "net.bytebuddy.implementation.bytecode",
-      "net.bytebuddy.implementation.bytecode.assign",
-      "net.bytebuddy.implementation.bytecode.assign.primitive",
-      "net.bytebuddy.implementation.bytecode.assign.reference",
-      "net.bytebuddy.implementation.bytecode.collection",
-      "net.bytebuddy.implementation.bytecode.constant",
-      "net.bytebuddy.implementation.bytecode.member",
-      "net.bytebuddy.jar.asm",
-      "net.bytebuddy.jar.asm.commons",
-      "net.bytebuddy.jar.asm.signature",
-      "net.bytebuddy.matcher",
-      "net.bytebuddy.pool",
-      "net.bytebuddy.utility",
-      "net.bytebuddy.utility.dispatcher",
-      "net.bytebuddy.utility.nullability",
-      "net.bytebuddy.utility.privilege",
-      "net.bytebuddy.utility.visitor"
-    ],
-    "net.bytebuddy:byte-buddy-agent": [
-      "net.bytebuddy.agent",
-      "net.bytebuddy.agent.utility.nullability"
-    ],
-    "org.checkerframework:checker-qual": [
-      "org.checkerframework.checker.builder.qual",
-      "org.checkerframework.checker.calledmethods.qual",
-      "org.checkerframework.checker.compilermsgs.qual",
-      "org.checkerframework.checker.fenum.qual",
-      "org.checkerframework.checker.formatter.qual",
-      "org.checkerframework.checker.guieffect.qual",
-      "org.checkerframework.checker.i18n.qual",
-      "org.checkerframework.checker.i18nformatter.qual",
-      "org.checkerframework.checker.index.qual",
-      "org.checkerframework.checker.initialization.qual",
-      "org.checkerframework.checker.interning.qual",
-      "org.checkerframework.checker.lock.qual",
-      "org.checkerframework.checker.mustcall.qual",
-      "org.checkerframework.checker.nullness.qual",
-      "org.checkerframework.checker.optional.qual",
-      "org.checkerframework.checker.propkey.qual",
-      "org.checkerframework.checker.regex.qual",
-      "org.checkerframework.checker.signature.qual",
-      "org.checkerframework.checker.signedness.qual",
-      "org.checkerframework.checker.tainting.qual",
-      "org.checkerframework.checker.units.qual",
-      "org.checkerframework.common.aliasing.qual",
-      "org.checkerframework.common.initializedfields.qual",
-      "org.checkerframework.common.reflection.qual",
-      "org.checkerframework.common.returnsreceiver.qual",
-      "org.checkerframework.common.subtyping.qual",
-      "org.checkerframework.common.util.count.report.qual",
-      "org.checkerframework.common.value.qual",
-      "org.checkerframework.dataflow.qual",
-      "org.checkerframework.framework.qual"
-    ],
     "org.hamcrest:hamcrest": [
       "org.hamcrest",
       "org.hamcrest.beans",
@@ -425,93 +231,10 @@
     ],
     "org.jspecify:jspecify": [
       "org.jspecify.annotations"
-    ],
-    "org.mockito:mockito-core": [
-      "org.mockito",
-      "org.mockito.codegen",
-      "org.mockito.configuration",
-      "org.mockito.creation.instance",
-      "org.mockito.exceptions.base",
-      "org.mockito.exceptions.misusing",
-      "org.mockito.exceptions.stacktrace",
-      "org.mockito.exceptions.verification",
-      "org.mockito.exceptions.verification.junit",
-      "org.mockito.exceptions.verification.opentest4j",
-      "org.mockito.hamcrest",
-      "org.mockito.internal",
-      "org.mockito.internal.configuration",
-      "org.mockito.internal.configuration.injection",
-      "org.mockito.internal.configuration.injection.filter",
-      "org.mockito.internal.configuration.injection.scanner",
-      "org.mockito.internal.configuration.plugins",
-      "org.mockito.internal.creation",
-      "org.mockito.internal.creation.bytebuddy",
-      "org.mockito.internal.creation.instance",
-      "org.mockito.internal.creation.proxy",
-      "org.mockito.internal.creation.settings",
-      "org.mockito.internal.creation.util",
-      "org.mockito.internal.debugging",
-      "org.mockito.internal.exceptions",
-      "org.mockito.internal.exceptions.stacktrace",
-      "org.mockito.internal.exceptions.util",
-      "org.mockito.internal.framework",
-      "org.mockito.internal.hamcrest",
-      "org.mockito.internal.handler",
-      "org.mockito.internal.invocation",
-      "org.mockito.internal.invocation.finder",
-      "org.mockito.internal.invocation.mockref",
-      "org.mockito.internal.junit",
-      "org.mockito.internal.listeners",
-      "org.mockito.internal.matchers",
-      "org.mockito.internal.matchers.apachecommons",
-      "org.mockito.internal.matchers.text",
-      "org.mockito.internal.progress",
-      "org.mockito.internal.reporting",
-      "org.mockito.internal.runners",
-      "org.mockito.internal.runners.util",
-      "org.mockito.internal.session",
-      "org.mockito.internal.stubbing",
-      "org.mockito.internal.stubbing.answers",
-      "org.mockito.internal.stubbing.defaultanswers",
-      "org.mockito.internal.util",
-      "org.mockito.internal.util.collections",
-      "org.mockito.internal.util.concurrent",
-      "org.mockito.internal.util.io",
-      "org.mockito.internal.util.reflection",
-      "org.mockito.internal.verification",
-      "org.mockito.internal.verification.api",
-      "org.mockito.internal.verification.argumentmatching",
-      "org.mockito.internal.verification.checkers",
-      "org.mockito.invocation",
-      "org.mockito.junit",
-      "org.mockito.listeners",
-      "org.mockito.mock",
-      "org.mockito.plugins",
-      "org.mockito.quality",
-      "org.mockito.session",
-      "org.mockito.stubbing",
-      "org.mockito.verification"
-    ],
-    "org.objenesis:objenesis": [
-      "org.objenesis",
-      "org.objenesis.instantiator",
-      "org.objenesis.instantiator.android",
-      "org.objenesis.instantiator.annotations",
-      "org.objenesis.instantiator.basic",
-      "org.objenesis.instantiator.gcj",
-      "org.objenesis.instantiator.perc",
-      "org.objenesis.instantiator.sun",
-      "org.objenesis.instantiator.util",
-      "org.objenesis.strategy"
-    ],
-    "org.ow2.asm:asm": [
-      "org.objectweb.asm",
-      "org.objectweb.asm.signature"
     ]
   },
   "repositories": {
     "https://repo1.maven.org/maven2/": [
-      "com.google.auto.value:auto-value-annotations",
       "com.google.code.findbugs:jsr305",
       "com.google.code.gson:gson",
       "com.google.errorprone:error_prone_annotations",
@@ -521,7 +244,6 @@
       "com.google.j2objc:j2objc-annotations",
       "com.google.protobuf:protobuf-java",
       "com.google.protobuf:protobuf-java-util",
-      "com.google.truth:truth",
       "io.netty:netty-tcnative-boringssl-static",
       "io.netty:netty-tcnative-boringssl-static:jar:linux-aarch_64",
       "io.netty:netty-tcnative-boringssl-static:jar:linux-x86_64",
@@ -529,16 +251,9 @@
       "io.netty:netty-tcnative-boringssl-static:jar:osx-x86_64",
       "io.netty:netty-tcnative-boringssl-static:jar:windows-x86_64",
       "io.netty:netty-tcnative-classes",
-      "junit:junit",
-      "net.bytebuddy:byte-buddy",
-      "net.bytebuddy:byte-buddy-agent",
-      "org.checkerframework:checker-qual",
       "org.hamcrest:hamcrest",
       "org.hamcrest:hamcrest-core",
-      "org.jspecify:jspecify",
-      "org.mockito:mockito-core",
-      "org.objenesis:objenesis",
-      "org.ow2.asm:asm"
+      "org.jspecify:jspecify"
     ]
   },
   "services": {},
diff --git a/private/extensions/maven.bzl b/private/extensions/maven.bzl
index c42a051..3236888 100644
--- a/private/extensions/maven.bzl
+++ b/private/extensions/maven.bzl
@@ -8,9 +8,10 @@
     "escape",
     "strip_packaging_and_classifier_and_version",
 )
-load("//private/lib:coordinates.bzl", "to_external_form", "unpack_coordinates")
+load("//private/lib:coordinates.bzl", "to_external_form", "to_key", "unpack_coordinates")
 load("//private/lib:toml_parser.bzl", "parse_toml")
 load("//private/rules:coursier.bzl", "DEFAULT_AAR_IMPORT_LABEL", "coursier_fetch", "pinned_coursier_fetch")
+load("//private/rules:maven_version.bzl", "compare_maven_versions")
 load("//private/rules:unpinned_maven_pin_command_alias.bzl", "unpinned_maven_pin_command_alias")
 load("//private/rules:v1_lock_file.bzl", "v1_lock_file")
 load("//private/rules:v2_lock_file.bzl", "v2_lock_file")
@@ -178,27 +179,11 @@
             to_return.append(exclusion)
     return to_return
 
-# Each bzlmod module may contribute jars to different rules_jvm_external maven repo namespaces.
-# We record this mapping of repo_name to the list of modules that contributed to it, and emit a warning
-# to the user if there are more than one module that contributed to the same repo name.
-#
-# This can be typical for the default @maven namespace, if a bzlmod dependency
-# wishes to contribute to the users' jars.
-def _check_repo_name(repo_name_2_module_name, repo_name, module_name):
-    contributing_module_names = repo_name_2_module_name.get(repo_name, [])
-    if module_name in contributing_module_names:
-        return
-    contributing_module_names.append(module_name)
-    repo_name_2_module_name[repo_name] = contributing_module_names
-
-def _warn_if_multiple_contributing_modules(repo_name_2_module_name, repos):
-    for (repo_name, contributing_module_names) in repo_name_2_module_name.items():
-        if len(contributing_module_names) == 1:
-            continue
-        known_contributing_modules = repos[repo_name].get("known_contributing_modules", sets.make())
-        new_contributing_modules = sets.difference(sets.make(contributing_module_names), known_contributing_modules)
-        if sets.length(new_contributing_modules) == 0:
-            continue
+def _warn_if_multiple_contributing_modules(repo, repo_name, non_root_bazel_dep_to_artifacts):
+    known_contributing_modules = repo.get("known_contributing_modules", sets.make())
+    contributing_module_names = non_root_bazel_dep_to_artifacts.keys()
+    new_contributing_modules = sets.difference(sets.make(contributing_module_names), known_contributing_modules)
+    if sets.length(new_contributing_modules) > 0:
         print("The maven repository '%s' has contributions from multiple bzlmod modules, and will be resolved together: %s." % (
                   repo_name,
                   sorted(contributing_module_names),
@@ -230,85 +215,67 @@
 
     return seen
 
-def _get_maven_module(artifact_spec):
-    """Extract group:artifact (maven module) coordinates from an artifact spec."""
-    return "%s:%s" % (artifact_spec.group, artifact_spec.artifact)
+def _deduplicate_non_root_artifacts(bazel_dep_to_non_root_artifacts, return_only_artifacts = False):
+    coordinate_to_artifact = {}
+    for bazel_dep_name in bazel_dep_to_non_root_artifacts:
+        for artifact in bazel_dep_to_non_root_artifacts.get(bazel_dep_name, []):
+            if not getattr(artifact, "testonly", False):
+                artifact_key = to_key(artifact)
 
-def _find_duplicate_artifacts_across_submodules(non_root_artifacts, root_maven_modules):
-    """Find artifacts that appear in multiple sub-modules with detailed version info."""
+                # prioritize highest version
+                if artifact_key in coordinate_to_artifact:
+                    _bazel_dep_name, current_artifact = coordinate_to_artifact[artifact_key]
+                    if compare_maven_versions(current_artifact.version, artifact.version) == -1:
+                        coordinate_to_artifact[artifact_key] = (bazel_dep_name, artifact)
+                else:
+                    coordinate_to_artifact[artifact_key] = (bazel_dep_name, artifact)
 
-    # Group artifacts by maven module (group:artifact)
-    maven_module_to_versions = {}
+    if return_only_artifacts:
+        return [v[1] for v in coordinate_to_artifact.values()]
+    else:
+        return coordinate_to_artifact
 
-    for artifact in non_root_artifacts:
-        maven_module = _get_maven_module(artifact)
-        if maven_module not in maven_module_to_versions:
-            maven_module_to_versions[maven_module] = []
-
-        # Extract version information
-        version = getattr(artifact, "version", "unspecified")
-        maven_module_to_versions[maven_module].append(version)
-
-    # Find duplicates that aren't overridden by root
-    duplicates = {}
-    for maven_module, versions in maven_module_to_versions.items():
-        # Only warn if:
-        # 1. There are multiple artifacts with the same group:artifact
-        # 2. This maven module is NOT overridden by the root module
-        if len(versions) > 1 and maven_module not in root_maven_modules:
-            # Deduplicate versions for cleaner output
-            unique_versions = {v: True for v in versions}.keys()
-            duplicates[maven_module] = sorted(unique_versions)
-
-    return duplicates
-
-def _deduplicate_artifacts_with_root_priority(name, root_artifacts, non_root_artifacts):
+# Each bzlmod module may contribute jars to different rules_jvm_external maven repo namespaces.
+# We emit a warning to the user if a module overrides an artifact version in the root maven repo.
+#
+# This can be typical for the default @maven namespace, if a bzlmod dependency
+# wishes to contribute to the users' jars.
+def _deduplicate_artifacts_with_root_priority(name, root_artifacts, bazel_dep_to_non_root_artifacts, repin_env_var, rje_verbose_env_var):
     """Deduplicate artifacts, giving priority to root module artifacts with force_version set."""
+    non_root_coordinate_to_artifact = _deduplicate_non_root_artifacts(bazel_dep_to_non_root_artifacts)
 
-    # Collect maven modules from root artifacts that have force_version = True
-    root_maven_modules_with_force_version = []
-    for artifact in root_artifacts:
-        if getattr(artifact, "force_version", False):
-            maven_module = _get_maven_module(artifact)
-            if maven_module not in root_maven_modules_with_force_version:
-                root_maven_modules_with_force_version.append(maven_module)
+    duplicate_artifact_warning = ""
+    filtered_non_root_artifacts = []
+    for root_artifact in root_artifacts:
+        artifact_key = to_key(root_artifact)
+        if artifact_key in non_root_coordinate_to_artifact:
+            bazel_dep_name, non_root_artifact = non_root_coordinate_to_artifact.pop(artifact_key)
+            if not getattr(root_artifact, "force_version", False):
+                # prioritize highest version
+                if compare_maven_versions(root_artifact.version, non_root_artifact.version) == -1:
+                    filtered_non_root_artifacts.append(non_root_artifact)
+                    duplicate_artifact_warning = duplicate_artifact_warning + (
+                        "\nWARNING: For dependency '%s:%s' the root @%s repo wants version %s, " % (root_artifact.group, root_artifact.artifact, name, root_artifact.version) +
+                        "but got %s from the %s bazel dep. " % (non_root_artifact.version, bazel_dep_name) +
+                        "Please update the version in your MODULE.bazel or set `force_version = True`."
+                    )
 
-    # Collect all maven modules from root artifacts (for duplicate detection)
-    all_root_maven_modules = []
-    for artifact in root_artifacts:
-        maven_module = _get_maven_module(artifact)
-        if maven_module not in all_root_maven_modules:
-            all_root_maven_modules.append(maven_module)
+    # Add any remaining non root artifacts that weren't found in the root artifact list
+    addtional_artifact_message = ""
+    for bazel_dep_name, non_root_artifact in non_root_coordinate_to_artifact.values():
+        addtional_artifact_message = addtional_artifact_message + (
+            "\nINFO: The @%s repo is getting the additional artifact %s:%s:%s from the %s bazel dep." % (name, non_root_artifact.group, non_root_artifact.artifact, non_root_artifact.version, bazel_dep_name)
+        )
+        filtered_non_root_artifacts.append(non_root_artifact)
 
-    # Find duplicates across sub-modules that aren't overridden by root
-    duplicate_submodule_artifacts = _find_duplicate_artifacts_across_submodules(
-        non_root_artifacts,
-        all_root_maven_modules,
-    )
+    if repin_env_var:
+        if duplicate_artifact_warning != "":
+            print(duplicate_artifact_warning)
+        if rje_verbose_env_var:
+            if addtional_artifact_message != "":
+                print(addtional_artifact_message)
 
-    # Filter non-root artifacts that conflict with root artifacts that have force_version = True
-    filtered_non_root = []
-    for artifact in non_root_artifacts:
-        maven_module = _get_maven_module(artifact)
-
-        # Only exclude if root module has force_version = True for this coordinate
-        if not maven_module in root_maven_modules_with_force_version:
-            filtered_non_root.append(artifact)
-
-    # Log detailed warning for duplicate sub-module artifacts
-    if len(duplicate_submodule_artifacts):
-        warning_parts = []
-        for maven_module, versions in duplicate_submodule_artifacts.items():
-            if len(versions) > 1:
-                warning_parts.append("%s (versions: %s)" % (maven_module, ", ".join(versions)))
-            else:
-                warning_parts.append(maven_module)
-
-        print("WARNING: The following coordinates from `%s` appear in multiple sub-modules with potentially different versions. " % name +
-              "Consider adding one of these to your root module to ensure consistent versions and setting `force_version = True` on that artifact:\n\t%s" %
-              "\n\t".join(sorted(warning_parts)))
-
-    return root_artifacts + filtered_non_root
+    return root_artifacts + filtered_non_root_artifacts
 
 def _amend_artifact(original_artifact, amend):
     """Apply amendments to an artifact struct, returning a new amended struct."""
@@ -391,137 +358,155 @@
 
     return artifacts, boms
 
-def _process_module_tags(mctx, mod, target_repos, repo_name_2_module_name):
-    """Process artifact and install tags for a single module."""
-
-    # Process from_toml tags
-    for from_toml_tag in mod.tags.from_toml:
-        _check_repo_name(repo_name_2_module_name, from_toml_tag.name, mod.name)
-
-        repo = target_repos.get(from_toml_tag.name, {})
-
-        content = mctx.read(mctx.path(from_toml_tag.libs_versions_toml))
-        parsed = parse_toml(content)
-
-        (new_artifacts, new_boms) = process_gradle_versions_file(parsed, from_toml_tag.bom_modules)
-
-        repo["artifacts"] = repo.get("artifacts", []) + new_artifacts
-        repo["boms"] = repo.get("boms", []) + new_boms
-        target_repos[from_toml_tag.name] = repo
-
-    for artifact in mod.tags.artifact:
-        _check_repo_name(repo_name_2_module_name, artifact.name, mod.name)
-
-        repo = target_repos.get(artifact.name, {})
-        existing_artifacts = repo.get("artifacts", [])
-        existing_artifacts.append(struct(
-            group = artifact.group,
-            artifact = artifact.artifact,
-            version = artifact.version,
-            packaging = artifact.packaging,
-            classifier = artifact.classifier,
-            force_version = artifact.force_version,
-            neverlink = artifact.neverlink,
-            testonly = artifact.testonly,
-            exclusions = _add_exclusions(artifact.exclusions),
-        ))
-
-        repo["artifacts"] = existing_artifacts
-        target_repos[artifact.name] = repo
-
-    for install in mod.tags.install:
-        _check_repo_name(repo_name_2_module_name, install.name, mod.name)
-
-        repo = target_repos.get(install.name, {})
-
-        repo["resolver"] = install.resolver
-
+def _add_artifacts_to_repo(repo, mod, new_artifacts):
+    if mod.is_root:
         artifacts = repo.get("artifacts", [])
-        repo["artifacts"] = artifacts + [unpack_coordinates(a) for a in install.artifacts]
+        repo["artifacts"] = artifacts + new_artifacts
+    else:
+        if not "bazel_dep_to_artifacts" in repo:
+            repo["bazel_dep_to_artifacts"] = {}
+        artifacts = repo["bazel_dep_to_artifacts"].get(mod.name, [])
+        repo["bazel_dep_to_artifacts"][mod.name] = artifacts + new_artifacts
 
+def _add_boms_to_repo(repo, mod, new_boms):
+    if mod.is_root:
         boms = repo.get("boms", [])
-        repo["boms"] = boms + [unpack_coordinates(b) for b in install.boms]
+        repo["boms"] = boms + new_boms
+    else:
+        if not "bazel_dep_to_boms" in repo:
+            repo["bazel_dep_to_boms"] = {}
+        boms = repo["bazel_dep_to_boms"].get(mod.name, [])
+        repo["bazel_dep_to_boms"][mod.name] = boms + new_boms
 
-        existing_repos = repo.get("repositories", [])
-        for repository in parse.parse_repository_spec_list(install.repositories):
-            repo_string = _json.write_repository_spec(repository)
-            if repo_string not in existing_repos:
-                existing_repos.append(repo_string)
-        repo["repositories"] = existing_repos
+def _process_module_tags(mctx):
+    """Process artifact and install tags for a single module."""
+    root_module_repos = {}
+    non_root_module_repos = {}
 
-        repo["excluded_artifacts"] = repo.get("excluded_artifacts", []) + install.excluded_artifacts
+    for mod in mctx.modules:
+        target_repos = root_module_repos if mod.is_root else non_root_module_repos
 
-        _logical_or(repo, "fetch_sources", False, install.fetch_sources)
-        _logical_or(repo, "generate_compat_repositories", False, install.generate_compat_repositories)
-        _logical_or(repo, "use_starlark_android_rules", False, install.use_starlark_android_rules)
-        _logical_or(repo, "ignore_empty_files", False, install.ignore_empty_files)
-        _logical_or(repo, "use_credentials_from_home_netrc_file", False, install.use_credentials_from_home_netrc_file)
+        # Process from_toml tags
+        for from_toml_tag in mod.tags.from_toml:
+            repo = target_repos.get(from_toml_tag.name, {})
 
-        repo["version_conflict_policy"] = _fail_if_different(
-            "version_conflict_policy",
-            repo.get("version_conflict_policy"),
-            install.version_conflict_policy,
-            [None, "default"],
-        )
+            content = mctx.read(mctx.path(from_toml_tag.libs_versions_toml))
+            parsed = parse_toml(content)
 
-        repo["strict_visibility_value"] = _fail_if_different(
-            "strict_visibility_value",
-            repo.get("strict_visibility_value", []),
-            install.strict_visibility_value,
-            [None, []],
-        )
+            (new_artifacts, new_boms) = process_gradle_versions_file(parsed, from_toml_tag.bom_modules)
 
-        additional_netrc_lines = repo.get("additional_netrc_lines", []) + getattr(install, "additional_netrc_lines", [])
-        repo["additional_netrc_lines"] = additional_netrc_lines
+            _add_artifacts_to_repo(repo, mod, new_artifacts)
+            _add_boms_to_repo(repo, mod, new_boms)
 
-        repo["aar_import_bzl_label"] = _fail_if_different(
-            "aar_import_bzl_label",
-            repo.get("aar_import_bzl_label"),
-            install.aar_import_bzl_label,
-            [DEFAULT_AAR_IMPORT_LABEL, None],
-        )
+            target_repos[from_toml_tag.name] = repo
 
-        repo["duplicate_version_warning"] = _fail_if_different(
-            "duplicate_version_warning",
-            repo.get("duplicate_version_warning"),
-            install.duplicate_version_warning,
-            [None, "warn"],
-        )
+        for artifact in mod.tags.artifact:
+            repo = target_repos.get(artifact.name, {})
+            new_artifact = struct(
+                group = artifact.group,
+                artifact = artifact.artifact,
+                version = artifact.version,
+                packaging = artifact.packaging,
+                classifier = artifact.classifier,
+                force_version = artifact.force_version,
+                neverlink = artifact.neverlink,
+                testonly = artifact.testonly,
+                exclusions = _add_exclusions(artifact.exclusions),
+            )
+            _add_artifacts_to_repo(repo, mod, [new_artifact])
 
-        # Get the longest timeout
-        timeout = repo.get("resolve_timeout", install.resolve_timeout)
-        if install.resolve_timeout > timeout:
-            timeout = install.resolve_timeout
-        repo["resolve_timeout"] = timeout
+            target_repos[artifact.name] = repo
 
-        if mod.is_root:
-            repo["repin_instructions"] = install.repin_instructions
-            repo["known_contributing_modules"] = sets.make(install.known_contributing_modules)
+        for install in mod.tags.install:
+            repo = target_repos.get(install.name, {})
 
-        repo["additional_coursier_options"] = repo.get("additional_coursier_options", []) + getattr(install, "additional_coursier_options", [])
+            repo["resolver"] = install.resolver
 
-        target_repos[install.name] = repo
+            _add_artifacts_to_repo(repo, mod, [unpack_coordinates(a) for a in install.artifacts])
+            _add_boms_to_repo(repo, mod, [unpack_coordinates(b) for b in install.boms])
 
-    # Process amend_artifact tags
-    for amend in mod.tags.amend_artifact:
-        _check_repo_name(repo_name_2_module_name, amend.name, mod.name)
+            existing_repos = repo.get("repositories", [])
+            for repository in parse.parse_repository_spec_list(install.repositories):
+                repo_string = _json.write_repository_spec(repository)
+                if repo_string not in existing_repos:
+                    existing_repos.append(repo_string)
+            repo["repositories"] = existing_repos
 
-        repo = target_repos.get(amend.name, {})
-        artifacts = repo.get("artifacts", [])
+            repo["excluded_artifacts"] = repo.get("excluded_artifacts", []) + install.excluded_artifacts
 
-        # Find matching artifacts and amend them
-        amended = False
-        for i, artifact in enumerate(artifacts):
-            if _coordinates_match(artifact, amend.coordinates):
-                artifacts[i] = _amend_artifact(artifact, amend)
-                amended = True
+            _logical_or(repo, "fetch_sources", False, install.fetch_sources)
+            _logical_or(repo, "generate_compat_repositories", False, install.generate_compat_repositories)
+            _logical_or(repo, "use_starlark_android_rules", False, install.use_starlark_android_rules)
+            _logical_or(repo, "ignore_empty_files", False, install.ignore_empty_files)
+            _logical_or(repo, "use_credentials_from_home_netrc_file", False, install.use_credentials_from_home_netrc_file)
 
-        if not amended:
-            # If no matching artifact found, this might be an error or we could create a placeholder
-            fail("No artifact found matching coordinates '%s' for amendment" % amend.coordinates)
+            repo["version_conflict_policy"] = _fail_if_different(
+                "version_conflict_policy",
+                repo.get("version_conflict_policy"),
+                install.version_conflict_policy,
+                [None, "default"],
+            )
 
-        repo["artifacts"] = artifacts
-        target_repos[amend.name] = repo
+            repo["strict_visibility_value"] = _fail_if_different(
+                "strict_visibility_value",
+                repo.get("strict_visibility_value", []),
+                install.strict_visibility_value,
+                [None, []],
+            )
+
+            additional_netrc_lines = repo.get("additional_netrc_lines", []) + getattr(install, "additional_netrc_lines", [])
+            repo["additional_netrc_lines"] = additional_netrc_lines
+
+            repo["aar_import_bzl_label"] = _fail_if_different(
+                "aar_import_bzl_label",
+                repo.get("aar_import_bzl_label"),
+                install.aar_import_bzl_label,
+                [DEFAULT_AAR_IMPORT_LABEL, None],
+            )
+
+            repo["duplicate_version_warning"] = _fail_if_different(
+                "duplicate_version_warning",
+                repo.get("duplicate_version_warning"),
+                install.duplicate_version_warning,
+                [None, "warn"],
+            )
+
+            # Get the longest timeout
+            timeout = repo.get("resolve_timeout", install.resolve_timeout)
+            if install.resolve_timeout > timeout:
+                timeout = install.resolve_timeout
+            repo["resolve_timeout"] = timeout
+
+            if mod.is_root:
+                repo["repin_instructions"] = install.repin_instructions
+                repo["known_contributing_modules"] = sets.make(install.known_contributing_modules)
+
+            repo["additional_coursier_options"] = repo.get("additional_coursier_options", []) + getattr(install, "additional_coursier_options", [])
+
+            target_repos[install.name] = repo
+
+        # Process amend_artifact tags
+        for amend in mod.tags.amend_artifact:
+            repo = target_repos.get(amend.name, {})
+            if mod.is_root:
+                artifacts = repo.get("artifacts", [])
+            else:
+                if not "bazel_dep_to_artifacts" in repo:
+                    repo["bazel_dep_to_artifacts"] = {}
+                artifacts = repo["bazel_dep_to_artifacts"].get(mod.name, [])
+
+            # Find matching artifacts and amend them
+            amended = False
+            for i, artifact in enumerate(artifacts):
+                if _coordinates_match(artifact, amend.coordinates):
+                    artifacts[i] = _amend_artifact(artifact, amend)
+                    amended = True
+
+            if not amended:
+                # If no matching artifact found, this might be an error or we could create a placeholder
+                fail("No artifact found matching coordinates '%s' for amendment" % amend.coordinates)
+
+    return root_module_repos, non_root_module_repos
 
 def _merge_repo_lists(root_list, non_root_list):
     """Merge two lists, removing duplicates while preserving order, root items first."""
@@ -548,11 +533,6 @@
     http_files = []
     compat_repos = []
 
-    # Separate tracking for root vs non-root artifacts
-    root_module_repos = {}
-    non_root_module_repos = {}
-    repo_name_2_module_name = {}
-
     # Process overrides first (they don't need deduplication)
     # The order of the transitive overrides do not matter, but the root
     # overrides take precedence over all transitive ones.
@@ -582,14 +562,16 @@
             overrides[override.name].update({override.coordinates: to_use})
             override_visibilities[override.name].update({override.coordinates: visibility_to_use})
 
-    # First pass: process the module tags, but keep root and non-root modules separately
-    for mod in mctx.modules:
-        collection = root_module_repos if mod.is_root else non_root_module_repos
-        _process_module_tags(mctx, mod, collection, repo_name_2_module_name)
+    # First pass: process the module tags, separating root and non-root modules
+    root_module_repos, non_root_module_repos = _process_module_tags(mctx)
 
     # Second pass: merge and deduplicate repositories
     all_repo_names = {name: True for name in root_module_repos.keys() + non_root_module_repos.keys()}.keys()
 
+    os_env = mctx.os.environ
+    repin_env_var = os_env.get("REPIN")
+    rje_verbose_env_var = os_env.get("RJE_VERBOSE")
+
     for repo_name in all_repo_names:
         root_repo = root_module_repos.get(repo_name, {})
         non_root_repo = non_root_module_repos.get(repo_name, {})
@@ -601,25 +583,32 @@
 
         # Special handling for artifacts and boms - deduplicate with root priority
         root_artifacts = root_repo.get("artifacts", [])
-        non_root_artifacts = non_root_repo.get("artifacts", [])
+        bazel_dep_to_non_root_artifacts = non_root_repo.get("bazel_dep_to_artifacts", {})
         root_boms = root_repo.get("boms", [])
-        non_root_boms = non_root_repo.get("boms", [])
+        non_root_bazel_dep_to_boms = non_root_repo.get("bazel_dep_to_boms", [])
 
         if repo_name in root_module_repos.keys():
+            # Warn users if multiple modules contribute to the same maven `name`
+            _warn_if_multiple_contributing_modules(root_repo, repo_name, bazel_dep_to_non_root_artifacts)
+
             merged_repo["artifacts"] = _deduplicate_artifacts_with_root_priority(
                 repo_name,
                 root_artifacts,
-                non_root_artifacts,
+                bazel_dep_to_non_root_artifacts,
+                repin_env_var,
+                rje_verbose_env_var,
             )
 
             merged_repo["boms"] = _deduplicate_artifacts_with_root_priority(
                 repo_name,
                 root_boms,
-                non_root_boms,
+                bazel_dep_to_non_root_artifacts,
+                repin_env_var,
+                rje_verbose_env_var,
             )
         else:
-            merged_repo["artifacts"] = non_root_artifacts
-            merged_repo["boms"] = non_root_boms
+            merged_repo["artifacts"] = _deduplicate_non_root_artifacts(bazel_dep_to_non_root_artifacts, True)
+            merged_repo["boms"] = _deduplicate_non_root_artifacts(bazel_dep_to_non_root_artifacts, True)
 
         # For list attributes, concatenate but avoid duplicates (root items first)
         for list_attr in ["repositories", "excluded_artifacts", "additional_netrc_lines", "additional_coursier_options"]:
@@ -629,9 +618,6 @@
 
         repos[repo_name] = merged_repo
 
-    # Warn users if multiple modules contribute to the same maven `name`
-    _warn_if_multiple_contributing_modules(repo_name_2_module_name, repos)
-
     # Breaking out the logic for picking lock files, because it's not terribly simple
     repo_to_lock_file = {}
     for mod in mctx.modules:
diff --git a/private/rules/maven_version.bzl b/private/rules/maven_version.bzl
new file mode 100644
index 0000000..e031d21
--- /dev/null
+++ b/private/rules/maven_version.bzl
@@ -0,0 +1,664 @@
+"""Maven ComparableVersion implementation in Starlark.
+
+This implements the version comparison algorithm from Apache Maven's
+ComparableVersion class. See:
+https://maven.apache.org/pom.html#version-order-specification
+
+Features:
+- Mixing of '-' (hyphen) and '.' (dot) separators
+- Transition between characters and digits constitutes a separator
+- Unlimited number of version components
+- Version components can be digits or strings
+- Well-known qualifiers have special ordering (alpha < beta < milestone < rc < snapshot < "" < sp)
+- Unknown qualifiers are sorted lexically after known qualifiers
+"""
+
+# Item type constants
+_TYPE_INT = "int"
+_TYPE_STR = "str"
+_TYPE_LIST = "list"
+_TYPE_COMBO = "combo"
+
+# Qualifier ordering (from least to greatest)
+# Index 5 ("") represents a release version
+_QUALIFIERS = ["alpha", "beta", "milestone", "rc", "snapshot", "", "sp"]
+
+# These qualifiers are equivalent to "" (release)
+_RELEASE_QUALIFIERS = ["ga", "final", "release"]
+
+# Aliases for qualifiers
+_ALIASES = {"cr": "rc"}
+
+def _strip_leading_zeroes(s):
+    """Strip leading zeroes from a numeric string."""
+    if not s:
+        return "0"
+    for i in range(len(s)):
+        if s[i] != "0":
+            return s[i:]
+    return "0"
+
+def _comparable_qualifier(qualifier):
+    """
+    Convert qualifier to a comparable string.
+
+    Known qualifiers get their index, unknown qualifiers get "7-" prefix
+    followed by the qualifier for lexical sorting after known ones.
+    """
+    if qualifier in _RELEASE_QUALIFIERS:
+        return str(_QUALIFIERS.index(""))
+
+    for i, q in enumerate(_QUALIFIERS):
+        if q == qualifier:
+            return str(i)
+
+    # Unknown qualifier: sort after known ones, lexically
+    return str(len(_QUALIFIERS)) + "-" + qualifier
+
+def _new_int_item(value):
+    """Create an integer item."""
+    return {"type": _TYPE_INT, "value": int(value) if value else 0}
+
+def _new_string_item(value, followed_by_digit):
+    """Create a string item, handling single-char aliases."""
+    if followed_by_digit and len(value) == 1:
+        if value == "a":
+            value = "alpha"
+        elif value == "b":
+            value = "beta"
+        elif value == "m":
+            value = "milestone"
+
+    # Apply aliases (cr -> rc)
+    value = _ALIASES.get(value, value)
+    return {"type": _TYPE_STR, "value": value}
+
+def _new_list_item(items):
+    """Create a list item."""
+    return {"type": _TYPE_LIST, "value": items}
+
+def _new_combo_item(string_part, digit_part):
+    """Create a combination item (string followed by digits, like 'alpha1')."""
+    return {"type": _TYPE_COMBO, "string_part": string_part, "digit_part": digit_part}
+
+def _parse_combination(buf):
+    """Parse a combination token like 'alpha1' or 'rc2'."""
+    buf = buf.replace("-", "")
+
+    # Find where digits start
+    index = len(buf)
+    for i in range(len(buf)):
+        if buf[i].isdigit():
+            index = i
+            break
+
+    string_part = _new_string_item(buf[:index], True)
+    digit_str = _strip_leading_zeroes(buf[index:]) if index < len(buf) else "0"
+    digit_part = _new_int_item(digit_str)
+
+    return _new_combo_item(string_part, digit_part)
+
+def _item_is_null(item):
+    """Check if an item is null (equivalent to empty/zero)."""
+    t = item["type"]
+    if t == _TYPE_INT:
+        return item["value"] == 0
+    elif t == _TYPE_STR:
+        return not item["value"]
+    elif t == _TYPE_LIST:
+        return len(item["value"]) == 0
+    elif t == _TYPE_COMBO:
+        return False
+    elif item["value"] == None:
+        return True
+    return False
+
+def _normalize_list(items):
+    """
+    Normalize a list by removing trailing null items.
+
+    This follows Maven's normalization rules:
+    - Remove trailing null items (0 for numbers, "" for strings, empty lists)
+    - But only if followed by a string or at the end
+    """
+    result = list(items)
+
+    # Iterate backwards using range
+    for idx in range(len(result) - 1, -1, -1):
+        item = result[idx]
+        if _item_is_null(item):
+            next_idx = idx + 1
+            should_remove = False
+
+            if next_idx >= len(result):
+                # At the end
+                should_remove = True
+            elif result[next_idx]["type"] == _TYPE_STR:
+                # Followed by string
+                should_remove = True
+            elif result[next_idx]["type"] == _TYPE_LIST:
+                # Followed by list - check first item in that list
+                next_list = result[next_idx]["value"]
+                if next_list:
+                    first_type = next_list[0]["type"]
+                    if first_type == _TYPE_COMBO or first_type == _TYPE_STR:
+                        should_remove = True
+                else:
+                    should_remove = True
+
+            if should_remove:
+                result.pop(idx)
+
+    return result
+
+def _compare_strings(s1, s2):
+    """Compare two strings lexicographically."""
+    if s1 < s2:
+        return -1
+    elif s1 > s2:
+        return 1
+    return 0
+
+def _compare_ints(i1, i2):
+    """Compare two integers."""
+    if i1 < i2:
+        return -1
+    elif i1 > i2:
+        return 1
+    return 0
+
+def _compare_to_null_simple(item):
+    """Compare a simple (non-list) item to null."""
+    t = item["type"]
+
+    if t == _TYPE_INT:
+        return 0 if item["value"] == 0 else 1
+
+    elif t == _TYPE_STR:
+        release_idx = _comparable_qualifier("")
+        return _compare_strings(_comparable_qualifier(item["value"]), release_idx)
+
+    elif t == _TYPE_COMBO:
+        # Combination's null comparison is based on its string part
+        release_idx = _comparable_qualifier("")
+        return _compare_strings(_comparable_qualifier(item["string_part"]["value"]), release_idx)
+
+    return 0
+
+def _compare_simple_items(left, right):
+    """
+    Compare two simple (non-list) items.
+
+    Returns:
+        -1 if left < right
+        0 if left == right
+        1 if left > right
+    """
+    lt = left["type"]
+    rt = right["type"]
+
+    # INT comparisons
+    if lt == _TYPE_INT:
+        if rt == _TYPE_INT:
+            return _compare_ints(left["value"], right["value"])
+        elif rt == _TYPE_STR:
+            return 1  # number > string
+        elif rt == _TYPE_COMBO:
+            return 1  # number > combination
+        elif rt == _TYPE_LIST:
+            return 1  # number > list
+
+        # STRING comparisons
+    elif lt == _TYPE_STR:
+        if rt == _TYPE_INT:
+            return -1  # string < number
+        elif rt == _TYPE_STR:
+            q1 = _comparable_qualifier(left["value"])
+            q2 = _comparable_qualifier(right["value"])
+            return _compare_strings(q1, q2)
+        elif rt == _TYPE_COMBO:
+            # Compare string to combination's string part
+            q1 = _comparable_qualifier(left["value"])
+            q2 = _comparable_qualifier(right["string_part"]["value"])
+            result = _compare_strings(q1, q2)
+            if result == 0:
+                return -1  # X < X1
+            return result
+        elif rt == _TYPE_LIST:
+            return -1  # string < list
+
+        # COMBINATION comparisons
+    elif lt == _TYPE_COMBO:
+        if rt == _TYPE_INT:
+            return -1  # combination < number
+        elif rt == _TYPE_STR:
+            q1 = _comparable_qualifier(left["string_part"]["value"])
+            q2 = _comparable_qualifier(right["value"])
+            result = _compare_strings(q1, q2)
+            if result == 0:
+                return 1  # X1 > X
+            return result
+        elif rt == _TYPE_COMBO:
+            q1 = _comparable_qualifier(left["string_part"]["value"])
+            q2 = _comparable_qualifier(right["string_part"]["value"])
+            result = _compare_strings(q1, q2)
+            if result == 0:
+                return _compare_ints(left["digit_part"]["value"], right["digit_part"]["value"])
+            return result
+        elif rt == _TYPE_LIST:
+            return -1  # combination < list
+
+        # LIST comparisons (comparing list to non-list)
+    elif lt == _TYPE_LIST:
+        if rt == _TYPE_INT:
+            return -1  # list < number
+        elif rt == _TYPE_STR:
+            return 1  # list > string
+        elif rt == _TYPE_COMBO:
+            return 1  # list > combination
+
+    return 0
+
+def _compare_items(left, right):
+    """
+    Compare two items iteratively using a stack.
+
+    Returns:
+        -1 if left < right
+        0 if left == right
+        1 if left > right
+    """
+
+    # Stack entries: (left_item, right_item, list_index)
+    # list_index is used when both items are lists to track position
+    stack = [(left, right, 0)]
+
+    # Process stack iteratively
+    for _ in range(1000):  # Safety limit to prevent infinite loops
+        if not stack:
+            return 0
+
+        current_left, current_right, idx = stack.pop()
+
+        # Handle null comparisons
+        if current_right == None:
+            if current_left == None:
+                continue
+
+            t = current_left["type"]
+            if t == _TYPE_LIST:
+                items = current_left["value"]
+                if not items:
+                    continue
+
+                # Push all items to compare to null (in reverse order so first is processed first)
+                for i in range(len(items) - 1, -1, -1):
+                    stack.append((items[i], None, 0))
+                continue
+            else:
+                result = _compare_to_null_simple(current_left)
+                if result != 0:
+                    return result
+                continue
+
+        if current_left == None:
+            # Invert comparison
+            t = current_right["type"]
+            if t == _TYPE_LIST:
+                items = current_right["value"]
+                if not items:
+                    continue
+
+                for i in range(len(items) - 1, -1, -1):
+                    stack.append((None, items[i], 0))
+                continue
+            else:
+                result = _compare_to_null_simple(current_right)
+                if result != 0:
+                    return -1 * result
+                continue
+
+        lt = current_left["type"]
+        rt = current_right["type"]
+
+        # Both are lists - compare element by element
+        if lt == _TYPE_LIST and rt == _TYPE_LIST:
+            left_items = current_left["value"]
+            right_items = current_right["value"]
+            max_len = max(len(left_items), len(right_items))
+
+            # Push remaining comparisons in reverse order
+            for i in range(max_len - 1, -1, -1):
+                l = left_items[i] if i < len(left_items) else None
+                r = right_items[i] if i < len(right_items) else None
+                stack.append((l, r, 0))
+            continue
+
+        # At least one is not a list - compare directly
+        result = _compare_simple_items(current_left, current_right)
+        if result != 0:
+            return result
+
+    return 0
+
+def _parse_version(version):
+    """
+    Parse a version string into a structured list.
+
+    Args:
+        version: The version string to parse
+
+    Returns:
+        A list item containing the parsed version structure
+    """
+    version = version.lower()
+
+    items = []
+    stack = [items]
+    current_list = [items]  # Use list to allow mutation in nested function scope
+
+    is_digit = [False]  # Use list to allow mutation
+    is_combination = [False]
+    start_index = [0]
+
+    def add_token(end_index):
+        """Add a token from start_index to end_index."""
+        if end_index == start_index[0]:
+            current_list[0].append(_new_int_item("0"))
+        else:
+            token = version[start_index[0]:end_index]
+            if is_combination[0]:
+                current_list[0].append(_parse_combination(token))
+            elif is_digit[0]:
+                current_list[0].append(_new_int_item(_strip_leading_zeroes(token)))
+            else:
+                current_list[0].append(_new_string_item(token, False))
+
+    def start_new_sublist():
+        """Start a new sub-list."""
+        if current_list[0]:
+            new_list = []
+            current_list[0].append(_new_list_item(new_list))
+            stack.append(new_list)
+            current_list[0] = new_list
+
+    # Process each character
+    skip_next = [False]
+    for i in range(len(version)):
+        if skip_next[0]:
+            skip_next[0] = False
+            continue
+
+        c = version[i]
+
+        if c == ".":
+            add_token(i)
+            is_combination[0] = False
+            start_index[0] = i + 1
+            is_digit[0] = False
+
+        elif c == "-":
+            if i == start_index[0]:
+                current_list[0].append(_new_int_item("0"))
+            else:
+                # Check for X-1 pattern (should be treated as X1 combination)
+                if not is_digit[0] and i < len(version) - 1:
+                    next_c = version[i + 1]
+                    if next_c.isdigit():
+                        is_combination[0] = True
+                        skip_next[0] = False  # Don't skip, continue processing
+                        continue
+
+                token = version[start_index[0]:i]
+                if is_combination[0]:
+                    current_list[0].append(_parse_combination(token))
+                elif is_digit[0]:
+                    current_list[0].append(_new_int_item(_strip_leading_zeroes(token)))
+                else:
+                    current_list[0].append(_new_string_item(token, False))
+
+            start_index[0] = i + 1
+
+            # Hyphen creates a new sub-list
+            start_new_sublist()
+
+            is_combination[0] = False
+            is_digit[0] = False
+
+        elif c.isdigit():
+            if not is_digit[0] and i > start_index[0]:
+                # Transition from string to digit: X1
+                is_combination[0] = True
+                start_new_sublist()
+            is_digit[0] = True
+
+        else:
+            # Non-digit character
+            if is_digit[0] and i > start_index[0]:
+                # Transition from digit to string
+                token = version[start_index[0]:i]
+                if is_combination[0]:
+                    current_list[0].append(_parse_combination(token))
+                else:
+                    current_list[0].append(_new_int_item(_strip_leading_zeroes(token)))
+
+                start_index[0] = i
+                start_new_sublist()
+                is_combination[0] = False
+
+            is_digit[0] = False
+
+    # Handle remaining token
+    if len(version) > start_index[0]:
+        # Treat .X as -X for any string qualifier X
+        if not is_digit[0] and current_list[0]:
+            start_new_sublist()
+
+        token = version[start_index[0]:]
+        if is_combination[0]:
+            current_list[0].append(_parse_combination(token))
+        elif is_digit[0]:
+            current_list[0].append(_new_int_item(_strip_leading_zeroes(token)))
+        else:
+            current_list[0].append(_new_string_item(token, False))
+
+    # Normalize all lists in reverse order (deepest first)
+    for j in range(len(stack) - 1, -1, -1):
+        lst = stack[j]
+        normalized = _normalize_list(lst)
+
+        # Clear and repopulate the list
+        for _ in range(len(lst)):
+            lst.pop()
+        for item in normalized:
+            lst.append(item)
+
+    return _new_list_item(items)
+
+def _simple_item_to_string(item):
+    """Convert a simple (non-list) item to string."""
+    t = item["type"]
+    if t == _TYPE_INT:
+        return str(item["value"])
+    elif t == _TYPE_STR:
+        return item["value"]
+    elif t == _TYPE_COMBO:
+        return item["string_part"]["value"] + str(item["digit_part"]["value"])
+    return ""
+
+def _item_to_canonical(item):
+    """
+    Convert an item to its canonical string representation (iterative).
+
+    Args:
+        item: The parsed item
+
+    Returns:
+        The canonical string representation
+    """
+    if item["type"] != _TYPE_LIST:
+        return _simple_item_to_string(item)
+
+    # For lists, we need to flatten and build the string iteratively
+    # Use a work queue approach: process list items and build result
+    result_parts = []
+
+    # Stack of (items_list, current_index, is_first_in_parent)
+    stack = [(item["value"], 0, True)]
+
+    for _ in range(1000):  # Safety limit
+        if not stack:
+            break
+
+        current_items, current_idx, is_first = stack[-1]
+
+        if current_idx >= len(current_items):
+            stack.pop()
+            continue
+
+        sub_item = current_items[current_idx]
+        stack[-1] = (current_items, current_idx + 1, False)
+
+        # Add separator if not first item
+        if not is_first:
+            if sub_item["type"] == _TYPE_LIST:
+                result_parts.append("-")
+            else:
+                result_parts.append(".")
+
+        if sub_item["type"] == _TYPE_LIST:
+            # Push the sub-list onto the stack
+            if sub_item["value"]:
+                stack.append((sub_item["value"], 0, True))
+        else:
+            result_parts.append(_simple_item_to_string(sub_item))
+
+    return "".join(result_parts)
+
+def get_canonical(version):
+    """
+    Get the canonical (normalized) form of a version string.
+
+    The canonical form normalizes the version by:
+    - Removing trailing zero segments (1.0.0 -> 1)
+    - Normalizing qualifiers (1-ga -> 1, 1a1 -> 1.alpha.1)
+    - Lowercasing everything
+
+    Args:
+        version: The version string
+
+    Returns:
+        The canonical form of the version
+    """
+    parsed = _parse_version(version)
+    return _item_to_canonical(parsed)
+
+def compare_maven_versions(v1, v2):
+    """
+    Compare two Maven version strings.
+
+    This implements Maven's ComparableVersion comparison algorithm.
+
+    Args:
+        v1: First version string
+        v2: Second version string
+
+    Returns:
+        -1 if v1 < v2
+        0 if v1 == v2
+        1 if v1 > v2
+    """
+    parsed1 = _parse_version(v1)
+    parsed2 = _parse_version(v2)
+    return _compare_items(parsed1, parsed2)
+
+def is_version_greater(v1, v2):
+    """Check if v1 is greater than v2."""
+    return compare_maven_versions(v1, v2) > 0
+
+def is_version_less(v1, v2):
+    """Check if v1 is less than v2."""
+    return compare_maven_versions(v1, v2) < 0
+
+def is_version_equal(v1, v2):
+    return compare_maven_versions(v1, v2) == 0
+
+def max_version(versions):
+    """
+    Return the maximum version from a list of version strings.
+
+    Args:
+        versions: List of version strings
+
+    Returns:
+        The maximum version string, or None if the list is empty
+    """
+    if not versions:
+        return None
+
+    result = versions[0]
+    for v in versions[1:]:
+        if compare_maven_versions(v, result) > 0:
+            result = v
+    return result
+
+def min_version(versions):
+    """
+    Return the minimum version from a list of version strings.
+
+    Args:
+        versions: List of version strings
+
+    Returns:
+        The minimum version string, or None if the list is empty
+    """
+    if not versions:
+        return None
+
+    result = versions[0]
+    for v in versions[1:]:
+        if compare_maven_versions(v, result) < 0:
+            result = v
+    return result
+
+def sort_versions(versions, reverse = False):
+    """
+    Sort a list of version strings.
+
+    This uses a simple insertion sort since Starlark doesn't have a built-in
+    sort with custom comparator.
+
+    Args:
+        versions: List of version strings
+        reverse: If True, sort in descending order
+
+    Returns:
+        A new sorted list of version strings
+    """
+    result = list(versions)
+
+    # Insertion sort using for loops
+    for i in range(1, len(result)):
+        key = result[i]
+
+        # Find insertion point
+        insert_pos = 0
+        found = False
+        for k in range(i - 1, -1, -1):
+            cmp = compare_maven_versions(result[k], key)
+            should_move = cmp > 0 if not reverse else cmp < 0
+            if not should_move:
+                insert_pos = k + 1
+                found = True
+                break
+
+        if not found:
+            insert_pos = 0
+
+        # Shift elements and insert
+        if insert_pos < i:
+            for m in range(i, insert_pos, -1):
+                result[m] = result[m - 1]
+            result[insert_pos] = key
+
+    return result
diff --git a/tests/custom_maven_install/multiple_lock_files_install.json b/tests/custom_maven_install/multiple_lock_files_install.json
index 3a698d6..6825e7e 100644
--- a/tests/custom_maven_install/multiple_lock_files_install.json
+++ b/tests/custom_maven_install/multiple_lock_files_install.json
@@ -1,6 +1,6 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
-  "__INPUT_ARTIFACTS_HASH": -965689370,
+  "__INPUT_ARTIFACTS_HASH": -575359670,
   "__RESOLVED_ARTIFACTS_HASH": -746459257,
   "artifacts": {
     "com.google.code.gson:gson": {
diff --git a/tests/custom_maven_install/regression_testing_coursier_install.json b/tests/custom_maven_install/regression_testing_coursier_install.json
index 037c9e1..d78bd9b 100644
--- a/tests/custom_maven_install/regression_testing_coursier_install.json
+++ b/tests/custom_maven_install/regression_testing_coursier_install.json
@@ -1,7 +1,7 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
   "__INPUT_ARTIFACTS_HASH": 899526900,
-  "__RESOLVED_ARTIFACTS_HASH": -1058194221,
+  "__RESOLVED_ARTIFACTS_HASH": -1034505854,
   "artifacts": {
     "android.arch.core:common": {
       "shasums": {
@@ -636,9 +636,9 @@
     },
     "com.nimbusds:nimbus-jose-jwt": {
       "shasums": {
-        "jar": "104ed6c348d35f68ab16d85550ed59239bcb7846bc2bd885ef3851a0ab2e73cf"
+        "jar": "899791e2fc5512279348617bd30c64f22d9a1456d7c54619e1f8ff359dcb2cc5"
       },
-      "version": "10.5"
+      "version": "10.7"
     },
     "com.nimbusds:oauth2-oidc-sdk": {
       "shasums": {
diff --git a/tests/custom_maven_install/root_wins_install.json b/tests/custom_maven_install/root_wins_install.json
index a5997f4..1d8b8ae 100644
--- a/tests/custom_maven_install/root_wins_install.json
+++ b/tests/custom_maven_install/root_wins_install.json
@@ -1,6 +1,6 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
-  "__INPUT_ARTIFACTS_HASH": -935842665,
+  "__INPUT_ARTIFACTS_HASH": 1721865497,
   "__RESOLVED_ARTIFACTS_HASH": -1272363274,
   "artifacts": {
     "io.netty:netty-buffer": {
diff --git a/tests/unit/BUILD b/tests/unit/BUILD
index 194f77c..4144fce 100644
--- a/tests/unit/BUILD
+++ b/tests/unit/BUILD
@@ -3,6 +3,7 @@
 load(":coursier_test.bzl", "coursier_test_suite")
 load(":coursier_utilities_test.bzl", "coursier_utilities_test_suite")
 load(":java_utilities_test.bzl", "java_utilities_test_suite")
+load(":maven_version_test.bzl", "maven_version_test_suite")
 load(":proxy_test.bzl", "proxy_test_suite")
 load(":specs_test.bzl", "artifact_specs_test_suite")
 load(":version_catalogs_test.bzl", "version_catalogs_test_suite")
@@ -19,6 +20,8 @@
 
 java_utilities_test_suite()
 
+maven_version_test_suite()
+
 proxy_test_suite()
 
 version_catalogs_test_suite()
diff --git a/tests/unit/maven_version_test.bzl b/tests/unit/maven_version_test.bzl
new file mode 100644
index 0000000..ce43958
--- /dev/null
+++ b/tests/unit/maven_version_test.bzl
@@ -0,0 +1,493 @@
+"""Tests for Maven version comparison."""
+
+load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
+load("//private/rules:maven_version.bzl", "compare_maven_versions", "get_canonical", "is_version_equal", "is_version_greater", "is_version_less", "max_version", "min_version", "sort_versions")
+
+def _check_versions_order(v1, v2):
+    """Check that v1 < v2."""
+    cmp = compare_maven_versions(v1, v2)
+    if cmp >= 0:
+        fail("Expected {} < {}, but got cmp = {}".format(v1, v2, cmp))
+
+    cmp_rev = compare_maven_versions(v2, v1)
+    if cmp_rev <= 0:
+        fail("Expected {} > {}, but got cmp = {}".format(v2, v1, cmp_rev))
+
+def _check_versions_equal(v1, v2):
+    """Check that v1 == v2."""
+    cmp = compare_maven_versions(v1, v2)
+    if cmp != 0:
+        fail("Expected {} == {}, but got cmp = {}".format(v1, v2, cmp))
+
+    cmp_rev = compare_maven_versions(v2, v1)
+    if cmp_rev != 0:
+        fail("Expected {} == {}, but got cmp = {}".format(v2, v1, cmp_rev))
+
+def _check_versions_array_order(versions):
+    """Check that versions are in ascending order."""
+    for i in range(1, len(versions)):
+        for j in range(i, len(versions)):
+            _check_versions_order(versions[i - 1], versions[j])
+
+# Test version arrays from Java tests
+_VERSIONS_QUALIFIER = [
+    "1-alpha2snapshot",
+    "1-alpha2",
+    "1-alpha-123",
+    "1-beta-2",
+    "1-beta123",
+    "1-m2",
+    "1-m11",
+    "1-rc",
+    "1-cr2",
+    "1-rc123",
+    "1-SNAPSHOT",
+    "1",
+    "1-sp",
+    "1-sp2",
+    "1-sp123",
+    "1-abc",
+    "1-def",
+    "1-pom-1",
+    "1-1-snapshot",
+    "1-1",
+    "1-2",
+    "1-123",
+]
+
+_VERSIONS_NUMBER = [
+    "2.0",
+    "2.0.a",
+    "2-1",
+    "2.0.2",
+    "2.0.123",
+    "2.1.0",
+    "2.1-a",
+    "2.1b",
+    "2.1-c",
+    "2.1-1",
+    "2.1.0.1",
+    "2.2",
+    "2.123",
+    "11.a2",
+    "11.a11",
+    "11.b2",
+    "11.b11",
+    "11.m2",
+    "11.m11",
+    "11",
+    "11.a",
+    "11b",
+    "11c",
+    "11m",
+]
+
+def _versions_qualifier_impl(ctx):
+    env = unittest.begin(ctx)
+    _check_versions_array_order(_VERSIONS_QUALIFIER)
+    return unittest.end(env)
+
+versions_qualifier_test = unittest.make(_versions_qualifier_impl)
+
+def _versions_number_impl(ctx):
+    env = unittest.begin(ctx)
+    _check_versions_array_order(_VERSIONS_NUMBER)
+    return unittest.end(env)
+
+versions_number_test = unittest.make(_versions_number_impl)
+
+def _versions_equal_impl(ctx):
+    env = unittest.begin(ctx)
+
+    _check_versions_equal("1", "1")
+    _check_versions_equal("1", "1.0")
+    _check_versions_equal("1", "1.0.0")
+    _check_versions_equal("1.0", "1.0.0")
+    _check_versions_equal("1", "1-0")
+    _check_versions_equal("1", "1.0-0")
+    _check_versions_equal("1.0", "1.0-0")
+
+    # no separator between number and character
+    _check_versions_equal("1a", "1-a")
+    _check_versions_equal("1a", "1.0-a")
+    _check_versions_equal("1a", "1.0.0-a")
+    _check_versions_equal("1.0a", "1-a")
+    _check_versions_equal("1.0.0a", "1-a")
+    _check_versions_equal("1x", "1-x")
+    _check_versions_equal("1x", "1.0-x")
+    _check_versions_equal("1x", "1.0.0-x")
+    _check_versions_equal("1.0x", "1-x")
+    _check_versions_equal("1.0.0x", "1-x")
+
+    # cr = rc
+    _check_versions_equal("1cr", "1rc")
+
+    # special "aliases" a, b and m for alpha, beta and milestone
+    _check_versions_equal("1a1", "1-alpha-1")
+    _check_versions_equal("1b2", "1-beta-2")
+    _check_versions_equal("1m3", "1-milestone-3")
+
+    # case insensitive
+    _check_versions_equal("1X", "1x")
+    _check_versions_equal("1A", "1a")
+    _check_versions_equal("1B", "1b")
+    _check_versions_equal("1M", "1m")
+    _check_versions_equal("1Cr", "1Rc")
+    _check_versions_equal("1cR", "1rC")
+    _check_versions_equal("1m3", "1Milestone3")
+    _check_versions_equal("1m3", "1MileStone3")
+    _check_versions_equal("1m3", "1MILESTONE3")
+
+    return unittest.end(env)
+
+versions_equal_test = unittest.make(_versions_equal_impl)
+
+def _versions_have_same_order_impl(ctx):
+    env = unittest.begin(ctx)
+
+    # These have same order (compare to 0) but may not be equal
+    asserts.equals(env, 0, compare_maven_versions("1ga", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1release", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1final", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1Ga", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1GA", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1RELEASE", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1release", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1RELeaSE", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1Final", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1FinaL", "1"))
+    asserts.equals(env, 0, compare_maven_versions("1FINAL", "1"))
+
+    return unittest.end(env)
+
+versions_have_same_order_test = unittest.make(_versions_have_same_order_impl)
+
+def _version_comparing_impl(ctx):
+    env = unittest.begin(ctx)
+
+    _check_versions_order("1", "2")
+    _check_versions_order("1.5", "2")
+    _check_versions_order("1", "2.5")
+    _check_versions_order("1.0", "1.1")
+    _check_versions_order("1.1", "1.2")
+    _check_versions_order("1.0.0", "1.1")
+    _check_versions_order("1.0.1", "1.1")
+    _check_versions_order("1.1", "1.2.0")
+
+    _check_versions_order("1.0-alpha-1", "1.0")
+    _check_versions_order("1.0-alpha-1", "1.0-alpha-2")
+    _check_versions_order("1.0-alpha-1", "1.0-beta-1")
+
+    _check_versions_order("1.0-beta-1", "1.0-SNAPSHOT")
+    _check_versions_order("1.0-SNAPSHOT", "1.0")
+    _check_versions_order("1.0-alpha-1-SNAPSHOT", "1.0-alpha-1")
+
+    _check_versions_order("1.0", "1.0-1")
+    _check_versions_order("1.0-1", "1.0-2")
+    _check_versions_order("1.0.0", "1.0-1")
+
+    _check_versions_order("2.0-1", "2.0.1")
+    _check_versions_order("2.0.1-klm", "2.0.1-lmn")
+    _check_versions_order("2.0.1", "2.0.1-xyz")
+    _check_versions_order("2.0.1", "2.0.1-123")
+    _check_versions_order("2.0.1-xyz", "2.0.1-123")
+
+    return unittest.end(env)
+
+version_comparing_test = unittest.make(_version_comparing_impl)
+
+def _leading_zeroes_impl(ctx):
+    env = unittest.begin(ctx)
+    _check_versions_order("0.7", "2")
+    _check_versions_order("0.2", "1.0.7")
+    return unittest.end(env)
+
+leading_zeroes_test = unittest.make(_leading_zeroes_impl)
+
+def _mng5568_impl(ctx):
+    """Test MNG-5568 edge case for transitive consistency."""
+    env = unittest.begin(ctx)
+
+    a = "6.1.0"
+    b = "6.1.0rc3"
+    c = "6.1H.5-beta"  # unusual version string, with 'H' in the middle
+
+    _check_versions_order(b, a)  # classical
+    _check_versions_order(b, c)  # now b < c
+    _check_versions_order(a, c)
+
+    return unittest.end(env)
+
+mng5568_test = unittest.make(_mng5568_impl)
+
+def _mng6572_impl(ctx):
+    """Test MNG-6572 optimization with large numbers."""
+    env = unittest.begin(ctx)
+
+    a = "20190126.230843"  # resembles a SNAPSHOT
+    b = "1234567890.12345"  # 10 digit number
+    c = "123456789012345.1H.5-beta"  # 15 digit number
+    d = "12345678901234567890.1H.5-beta"  # 20 digit number
+
+    _check_versions_order(a, b)
+    _check_versions_order(b, c)
+    _check_versions_order(a, c)
+    _check_versions_order(c, d)
+    _check_versions_order(b, d)
+    _check_versions_order(a, d)
+
+    return unittest.end(env)
+
+mng6572_test = unittest.make(_mng6572_impl)
+
+def _version_equal_with_leading_zeroes_impl(ctx):
+    """Test all versions are equal when starting with leading zeroes."""
+    env = unittest.begin(ctx)
+
+    versions = [
+        "0000000000000000001",
+        "000000000000000001",
+        "00000000000000001",
+        "0000000000000001",
+        "000000000000001",
+        "00000000000001",
+        "0000000000001",
+        "000000000001",
+        "00000000001",
+        "0000000001",
+        "000000001",
+        "00000001",
+        "0000001",
+        "000001",
+        "00001",
+        "0001",
+        "001",
+        "01",
+        "1",
+    ]
+
+    for i in range(len(versions)):
+        for j in range(i, len(versions)):
+            _check_versions_equal(versions[i], versions[j])
+
+    return unittest.end(env)
+
+version_equal_with_leading_zeroes_test = unittest.make(_version_equal_with_leading_zeroes_impl)
+
+def _version_zero_equal_with_leading_zeroes_impl(ctx):
+    """Test all '0' versions are equal with leading zeroes."""
+    env = unittest.begin(ctx)
+
+    versions = [
+        "0000000000000000000",
+        "000000000000000000",
+        "00000000000000000",
+        "0000000000000000",
+        "000000000000000",
+        "00000000000000",
+        "0000000000000",
+        "000000000000",
+        "00000000000",
+        "0000000000",
+        "000000000",
+        "00000000",
+        "0000000",
+        "000000",
+        "00000",
+        "0000",
+        "000",
+        "00",
+        "0",
+    ]
+
+    for i in range(len(versions)):
+        for j in range(i, len(versions)):
+            _check_versions_equal(versions[i], versions[j])
+
+    return unittest.end(env)
+
+version_zero_equal_with_leading_zeroes_test = unittest.make(_version_zero_equal_with_leading_zeroes_impl)
+
+def _mng6964_impl(ctx):
+    """Test MNG-6964 edge cases for qualifiers starting with '-0.'."""
+    env = unittest.begin(ctx)
+
+    a = "1-0.alpha"
+    b = "1-0.beta"
+    c = "1"
+
+    _check_versions_order(a, c)  # a < c
+    _check_versions_order(b, c)  # b < c
+    _check_versions_order(a, b)  # a < b
+
+    return unittest.end(env)
+
+mng6964_test = unittest.make(_mng6964_impl)
+
+def _locale_independent_impl(ctx):
+    """Test case insensitivity works for all letters."""
+    env = unittest.begin(ctx)
+    _check_versions_equal("1-abcdefghijklmnopqrstuvwxyz", "1-ABCDEFGHIJKLMNOPQRSTUVWXYZ")
+    return unittest.end(env)
+
+locale_independent_test = unittest.make(_locale_independent_impl)
+
+def _mng7644_impl(ctx):
+    """Test MNG-7644: 1.0.0.X1 < 1.0.0-X2 for any string X."""
+    env = unittest.begin(ctx)
+
+    for x in ["abc", "alpha", "a", "beta", "b", "def", "milestone", "m", "RC"]:
+        # 1.0.0.X1 < 1.0.0-X2 for any string x
+        _check_versions_order("1.0.0." + x + "1", "1.0.0-" + x + "2")
+
+        # 2.0.X == 2-X == 2.0.0.X for any string x
+        _check_versions_equal("2-" + x, "2.0." + x)
+        _check_versions_equal("2-" + x, "2.0.0." + x)
+        _check_versions_equal("2.0." + x, "2.0.0." + x)
+
+    return unittest.end(env)
+
+mng7644_test = unittest.make(_mng7644_impl)
+
+def _mng7714_impl(ctx):
+    """Test MNG-7714: sp qualifier ordering with redhat suffix."""
+    env = unittest.begin(ctx)
+
+    f = "1.0.final-redhat"
+    sp1 = "1.0-sp1-redhat"
+    sp2 = "1.0-sp-1-redhat"
+    sp3 = "1.0-sp.1-redhat"
+
+    _check_versions_order(f, sp1)
+    _check_versions_order(f, sp2)
+    _check_versions_order(f, sp3)
+
+    return unittest.end(env)
+
+mng7714_test = unittest.make(_mng7714_impl)
+
+def _helper_functions_impl(ctx):
+    """Test helper functions like max_version, min_version, sort_versions."""
+    env = unittest.begin(ctx)
+
+    versions = ["1.0", "2.0", "1.5", "1.0-alpha", "2.0-SNAPSHOT"]
+
+    asserts.equals(env, "2.0", max_version(versions))
+    asserts.equals(env, "1.0-alpha", min_version(versions))
+
+    sorted_versions = sort_versions(versions)
+    asserts.equals(env, ["1.0-alpha", "1.0", "1.5", "2.0-SNAPSHOT", "2.0"], sorted_versions)
+
+    sorted_desc = sort_versions(versions, reverse = True)
+    asserts.equals(env, ["2.0", "2.0-SNAPSHOT", "1.5", "1.0", "1.0-alpha"], sorted_desc)
+
+    asserts.true(env, is_version_greater("2.0", "1.0"))
+    asserts.true(env, is_version_less("1.0", "2.0"))
+    asserts.true(env, is_version_equal("1.0", "1.0.0"))
+
+    return unittest.end(env)
+
+helper_functions_test = unittest.make(_helper_functions_impl)
+
+def _compare_digit_to_letter_impl(ctx):
+    """Test that digits are greater than letters."""
+    env = unittest.begin(ctx)
+
+    asserts.true(env, compare_maven_versions("7", "J") > 0)
+    asserts.true(env, compare_maven_versions("J", "7") < 0)
+    asserts.true(env, compare_maven_versions("7", "c") > 0)
+    asserts.true(env, compare_maven_versions("c", "7") < 0)
+
+    return unittest.end(env)
+
+compare_digit_to_letter_test = unittest.make(_compare_digit_to_letter_impl)
+
+def _lexicographic_order_impl(ctx):
+    """Test lexicographic ordering of unknown qualifiers."""
+    env = unittest.begin(ctx)
+
+    asserts.true(env, compare_maven_versions("zebra", "aardvark") > 0)
+    asserts.true(env, compare_maven_versions("aardvark", "zebra") < 0)
+
+    return unittest.end(env)
+
+lexicographic_order_test = unittest.make(_lexicographic_order_impl)
+
+def _case_insensitive_impl(ctx):
+    """Test case insensitivity."""
+    env = unittest.begin(ctx)
+
+    asserts.equals(env, 0, compare_maven_versions("1.0.0-ALPHA1", "1.0.0-alpha1"))
+    asserts.equals(env, 0, compare_maven_versions("1.0.0-alpha1", "1.0.0-ALPHA1"))
+    asserts.equals(env, 0, compare_maven_versions("1.A", "1.a"))
+    asserts.equals(env, 0, compare_maven_versions("1.a", "1.A"))
+
+    return unittest.end(env)
+
+case_insensitive_test = unittest.make(_case_insensitive_impl)
+
+def _get_canonical_impl(ctx):
+    """Test canonical form generation."""
+    env = unittest.begin(ctx)
+
+    # Basic normalization - trailing zeros removed
+    asserts.equals(env, "1", get_canonical("1.0.0"))
+    asserts.equals(env, "1", get_canonical("1.0"))
+    asserts.equals(env, "1", get_canonical("1"))
+
+    # Qualifiers normalized - note: a/b/m only expand to alpha/beta/milestone
+    # when followed by a digit (like "a1" -> "alpha1")
+    asserts.equals(env, "1-alpha", get_canonical("1.0-alpha"))
+    asserts.equals(env, "1-a", get_canonical("1-a"))  # standalone 'a' stays as 'a'
+    asserts.equals(env, "1-b", get_canonical("1-b"))  # standalone 'b' stays as 'b'
+    asserts.equals(env, "1-m", get_canonical("1-m"))  # standalone 'm' stays as 'm'
+    asserts.equals(env, "1-rc", get_canonical("1-cr"))  # cr -> rc alias always applies
+
+    # Aliases expand when followed by digit
+    asserts.equals(env, "1-alpha1", get_canonical("1-a1"))
+    asserts.equals(env, "1-beta2", get_canonical("1-b2"))
+    asserts.equals(env, "1-milestone3", get_canonical("1-m3"))
+
+    # Case normalized to lowercase
+    asserts.equals(env, "1-alpha", get_canonical("1-ALPHA"))
+    asserts.equals(env, "1-beta", get_canonical("1-BETA"))
+
+    # MNG-7700 test cases
+    asserts.equals(env, "x", get_canonical("0.x"))
+    asserts.equals(env, "x", get_canonical("0-x"))
+    asserts.equals(env, "rc", get_canonical("0.rc"))
+    asserts.equals(env, "0-1", get_canonical("0-1"))
+    asserts.equals(env, "0.2", get_canonical("0.2"))
+
+    # Canonical of canonical should equal itself
+    for v in ["1.0-alpha", "2.3.4", "1-SNAPSHOT", "1.0.0-RC1"]:
+        canonical = get_canonical(v)
+        asserts.equals(env, canonical, get_canonical(canonical))
+
+    return unittest.end(env)
+
+get_canonical_test = unittest.make(_get_canonical_impl)
+
+def maven_version_test_suite():
+    unittest.suite(
+        "maven_version_tests",
+        versions_qualifier_test,
+        versions_number_test,
+        versions_equal_test,
+        versions_have_same_order_test,
+        version_comparing_test,
+        leading_zeroes_test,
+        mng5568_test,
+        mng6572_test,
+        version_equal_with_leading_zeroes_test,
+        version_zero_equal_with_leading_zeroes_test,
+        mng6964_test,
+        locale_independent_test,
+        mng7644_test,
+        mng7714_test,
+        helper_functions_test,
+        compare_digit_to_letter_test,
+        lexicographic_order_test,
+        case_insensitive_test,
+        get_canonical_test,
+    )