fix: honour pinned versions in layered coursier installs (#1631)
diff --git a/MODULE.bazel b/MODULE.bazel
index 22e50a9..0c9502e 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -1076,6 +1076,18 @@
     resolver = "coursier",
 )
 
+dev_maven.install(
+    name = "coursier_pinned_layering",
+    artifacts = [
+        "com.google.code.findbugs:jsr305:3.0.1",
+    ],
+    known_contributing_modules = [
+        "coursier_higher_layer",
+    ],
+    resolver = "coursier",
+    version_conflict_policy = "pinned",
+)
+
 bazel_dep(name = "coursier_higher_layer", version = "0.0.0", dev_dependency = True)
 local_path_override(
     module_name = "coursier_higher_layer",
@@ -1117,6 +1129,7 @@
     "amend_artifacts",
     "bom_only_pinning",
     "coursier_layering",
+    "coursier_pinned_layering",
     "duplicate_version_error",
     "duplicate_version_warning",
     "duplicate_version_warning_same_version",
diff --git a/docs/bzlmod-api.md b/docs/bzlmod-api.md
index 38e7474..0a29f54 100644
--- a/docs/bzlmod-api.md
+++ b/docs/bzlmod-api.md
@@ -114,7 +114,7 @@
 | <a id="maven.install-strict_visibility_value"></a>strict_visibility_value |  -   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional |  `["@rules_jvm_external//visibility:private"]`  |
 | <a id="maven.install-use_credentials_from_home_netrc_file"></a>use_credentials_from_home_netrc_file |  Whether to pass machine login credentials from the ~/.netrc file to coursier.   | Boolean | optional |  `False`  |
 | <a id="maven.install-use_starlark_android_rules"></a>use_starlark_android_rules |  Whether to use the native or Starlark version of the Android rules.   | Boolean | optional |  `False`  |
-| <a id="maven.install-version_conflict_policy"></a>version_conflict_policy |  Policy for user-defined vs. transitive dependency version conflicts<br><br>If "pinned", choose the user-specified version in maven_install unconditionally. With the Gradle and Maven resolvers, this only applies to artifacts contributed by the root module. If "default", follow the selected resolver's default policy.   | String | optional |  `"default"`  |
+| <a id="maven.install-version_conflict_policy"></a>version_conflict_policy |  Policy for user-defined vs. transitive dependency version conflicts<br><br>If "pinned", choose the user-specified version in maven_install unconditionally. In layered installs, artifacts contributed by the root module take precedence over versions contributed by other modules. If "default", follow the selected resolver's default policy.   | String | optional |  `"default"`  |
 
 <a id="maven.override"></a>
 
diff --git a/docs/bzlmod.md b/docs/bzlmod.md
index 400c00c..7636db1 100644
--- a/docs/bzlmod.md
+++ b/docs/bzlmod.md
@@ -216,16 +216,16 @@
 "Highest" uses the Maven `ComparableVersion` ordering implemented by
 `private/rules/maven_version.bzl`, not lexical string ordering.
 
-`version_conflict_policy = "pinned"` changes this interaction. For the Gradle and Maven resolvers,
-root artifacts are marked as `force_version` before layering. The duplicate-force check applies to
-declared forces before this policy is applied. Maven then marks every versioned root declaration.
-Gradle first selects one version for each root `group:artifact`: an unclassified declaration takes
-precedence over classified declarations, and Maven `ComparableVersion` order selects among
-declarations with the same classification status. Every root declaration for that module at the
-selected version is then marked forced, including classified declarations. The root consequently
-wins because it now forces the coordinate. For Coursier, layering is unchanged and the one
-surviving direct version is later passed as a `--force-version` argument. A higher non-root
-version can therefore displace the root under Coursier and then be pinned.
+`version_conflict_policy = "pinned"` changes this interaction. Root artifacts are marked as
+`force_version` before layering, whatever the resolver. The duplicate-force check applies to
+declared forces before this policy is applied. Maven and Coursier mark every versioned root
+declaration. Gradle first selects one version for each root `group:artifact`: an unclassified
+declaration takes precedence over classified declarations, and Maven `ComparableVersion` order
+selects among declarations with the same classification status. Every root declaration for that
+module at the selected version is then marked forced, including classified declarations. The
+root consequently wins because it now forces the coordinate. Coursier additionally passes every
+direct artifact's version as a `--force-version` argument, so transitive dependencies cannot
+displace the pinned version either.
 
 The `force_version` flag can be set by an `artifact` tag, an `amend_artifact` tag, or a regular
 artifact read by `from_toml`. Coordinates in `install.artifacts` cannot carry the flag. BOMs use the
diff --git a/examples/android_local_test/maven_install.json b/examples/android_local_test/maven_install.json
index 2734089..0115c75 100644
--- a/examples/android_local_test/maven_install.json
+++ b/examples/android_local_test/maven_install.json
@@ -1,24 +1,24 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
   "__INPUT_ARTIFACTS_HASH": {
-    "androidx.annotation:annotation": 588494489,
-    "androidx.appcompat:appcompat": -864916242,
-    "androidx.emoji2:emoji2": -667316066,
-    "androidx.emoji2:emoji2-views-helper": -799295894,
-    "androidx.lifecycle:lifecycle-common": 965287242,
-    "androidx.lifecycle:lifecycle-livedata-core": -1534752455,
-    "androidx.lifecycle:lifecycle-runtime": -1133978085,
-    "androidx.lifecycle:lifecycle-viewmodel-savedstate": -1098900718,
-    "androidx.savedstate:savedstate": -833725607,
-    "androidx.test.ext:junit": 923457984,
+    "androidx.annotation:annotation": 1998854284,
+    "androidx.appcompat:appcompat": 526874845,
+    "androidx.emoji2:emoji2": 1270171825,
+    "androidx.emoji2:emoji2-views-helper": -985278619,
+    "androidx.lifecycle:lifecycle-common": -84119015,
+    "androidx.lifecycle:lifecycle-livedata-core": 993085898,
+    "androidx.lifecycle:lifecycle-runtime": 1906619816,
+    "androidx.lifecycle:lifecycle-viewmodel-savedstate": -577646767,
+    "androidx.savedstate:savedstate": -671028048,
+    "androidx.test.ext:junit": 199011195,
     "com.google.code.findbugs:jsr305": 495355163,
-    "com.google.code.gson:gson": 2063056812,
-    "com.google.errorprone:error_prone_annotations": 1790047777,
-    "com.google.guava:guava": -249069712,
-    "com.google.j2objc:j2objc-annotations": -26400317,
-    "junit:junit": -744267592,
-    "org.assertj:assertj-core": -1145412507,
-    "org.robolectric:robolectric": -1784339344,
+    "com.google.code.gson:gson": 579810017,
+    "com.google.errorprone:error_prone_annotations": 1828684942,
+    "com.google.guava:guava": 1001533771,
+    "com.google.j2objc:j2objc-annotations": -57684360,
+    "junit:junit": 1633899997,
+    "org.assertj:assertj-core": -88843862,
+    "org.robolectric:robolectric": 1761992425,
     "repositories": 1670674627
   },
   "__RESOLVED_ARTIFACTS_HASH": {
@@ -1140,13 +1140,40 @@
     ]
   },
   "packages": {
+    "androidx.activity:activity:aar": [
+      "androidx.activity",
+      "androidx.activity.contextaware",
+      "androidx.activity.result",
+      "androidx.activity.result.contract"
+    ],
+    "androidx.annotation:annotation-experimental:aar": [
+      "androidx.annotation",
+      "androidx.annotation.experimental"
+    ],
     "androidx.annotation:annotation-jvm": [
       "androidx.annotation"
     ],
+    "androidx.appcompat:appcompat-resources:aar": [
+      "androidx.appcompat.content.res",
+      "androidx.appcompat.graphics.drawable",
+      "androidx.appcompat.resources",
+      "androidx.appcompat.widget"
+    ],
+    "androidx.appcompat:appcompat:aar": [
+      "androidx.appcompat.app",
+      "androidx.appcompat.graphics.drawable",
+      "androidx.appcompat.text",
+      "androidx.appcompat.view",
+      "androidx.appcompat.view.menu",
+      "androidx.appcompat.widget"
+    ],
     "androidx.arch.core:core-common": [
       "androidx.arch.core.internal",
       "androidx.arch.core.util"
     ],
+    "androidx.arch.core:core-runtime:aar": [
+      "androidx.arch.core.executor"
+    ],
     "androidx.collection:collection-jvm": [
       "androidx.collection",
       "androidx.collection.internal"
@@ -1157,12 +1184,196 @@
     "androidx.concurrent:concurrent-futures-ktx": [
       "androidx.concurrent.futures"
     ],
+    "androidx.core:core-ktx:aar": [
+      "androidx.core.animation",
+      "androidx.core.content",
+      "androidx.core.content.res",
+      "androidx.core.database",
+      "androidx.core.database.sqlite",
+      "androidx.core.graphics",
+      "androidx.core.graphics.drawable",
+      "androidx.core.location",
+      "androidx.core.net",
+      "androidx.core.os",
+      "androidx.core.text",
+      "androidx.core.transition",
+      "androidx.core.util",
+      "androidx.core.view",
+      "androidx.core.widget"
+    ],
+    "androidx.core:core-viewtree:aar": [
+      "androidx.core.viewtree"
+    ],
+    "androidx.core:core:aar": [
+      "android.support.v4.app",
+      "android.support.v4.graphics.drawable",
+      "android.support.v4.os",
+      "androidx.core.accessibilityservice",
+      "androidx.core.app",
+      "androidx.core.app.unusedapprestrictions",
+      "androidx.core.content",
+      "androidx.core.content.pm",
+      "androidx.core.content.res",
+      "androidx.core.database",
+      "androidx.core.database.sqlite",
+      "androidx.core.graphics",
+      "androidx.core.graphics.drawable",
+      "androidx.core.hardware.display",
+      "androidx.core.hardware.fingerprint",
+      "androidx.core.internal.view",
+      "androidx.core.location",
+      "androidx.core.math",
+      "androidx.core.net",
+      "androidx.core.os",
+      "androidx.core.provider",
+      "androidx.core.service.quicksettings",
+      "androidx.core.telephony",
+      "androidx.core.telephony.mbms",
+      "androidx.core.text",
+      "androidx.core.text.method",
+      "androidx.core.text.util",
+      "androidx.core.util",
+      "androidx.core.view",
+      "androidx.core.view.accessibility",
+      "androidx.core.view.animation",
+      "androidx.core.view.autofill",
+      "androidx.core.view.contentcapture",
+      "androidx.core.view.inputmethod",
+      "androidx.core.widget"
+    ],
+    "androidx.cursoradapter:cursoradapter:aar": [
+      "androidx.cursoradapter.widget"
+    ],
+    "androidx.customview:customview:aar": [
+      "androidx.customview.view",
+      "androidx.customview.widget"
+    ],
+    "androidx.drawerlayout:drawerlayout:aar": [
+      "androidx.drawerlayout.widget"
+    ],
+    "androidx.emoji2:emoji2-views-helper:aar": [
+      "androidx.emoji2.viewsintegration"
+    ],
+    "androidx.emoji2:emoji2:aar": [
+      "androidx.emoji2.text",
+      "androidx.emoji2.text.flatbuffer"
+    ],
+    "androidx.fragment:fragment:aar": [
+      "androidx.fragment.app",
+      "androidx.fragment.app.strictmode"
+    ],
+    "androidx.interpolator:interpolator:aar": [
+      "androidx.interpolator.view.animation"
+    ],
     "androidx.lifecycle:lifecycle-common-jvm": [
       "androidx.lifecycle"
     ],
+    "androidx.lifecycle:lifecycle-livedata-core:aar": [
+      "androidx.lifecycle"
+    ],
+    "androidx.lifecycle:lifecycle-livedata:aar": [
+      "androidx.lifecycle"
+    ],
+    "androidx.lifecycle:lifecycle-process:aar": [
+      "androidx.lifecycle"
+    ],
+    "androidx.lifecycle:lifecycle-runtime-android:aar": [
+      "androidx.lifecycle"
+    ],
+    "androidx.lifecycle:lifecycle-viewmodel-android:aar": [
+      "androidx.lifecycle",
+      "androidx.lifecycle.viewmodel",
+      "androidx.lifecycle.viewmodel.internal"
+    ],
+    "androidx.lifecycle:lifecycle-viewmodel-savedstate-android:aar": [
+      "androidx.lifecycle",
+      "androidx.lifecycle.internal",
+      "androidx.lifecycle.serialization"
+    ],
+    "androidx.loader:loader:aar": [
+      "androidx.loader.app",
+      "androidx.loader.content"
+    ],
+    "androidx.profileinstaller:profileinstaller:aar": [
+      "androidx.profileinstaller"
+    ],
     "androidx.resourceinspection:resourceinspection-annotation": [
       "androidx.resourceinspection.annotation"
     ],
+    "androidx.savedstate:savedstate-android:aar": [
+      "androidx.savedstate",
+      "androidx.savedstate.internal",
+      "androidx.savedstate.serialization",
+      "androidx.savedstate.serialization.serializers"
+    ],
+    "androidx.startup:startup-runtime:aar": [
+      "androidx.startup"
+    ],
+    "androidx.test.espresso:espresso-idling-resource:aar": [
+      "androidx.test.espresso",
+      "androidx.test.espresso.idling"
+    ],
+    "androidx.test.ext:junit:aar": [
+      "androidx.test.ext.junit.rules",
+      "androidx.test.ext.junit.runners"
+    ],
+    "androidx.test.services:storage:aar": [
+      "androidx.test.services.storage",
+      "androidx.test.services.storage.file",
+      "androidx.test.services.storage.internal"
+    ],
+    "androidx.test:core:aar": [
+      "androidx.test.core.app",
+      "androidx.test.core.content.pm",
+      "androidx.test.core.graphics",
+      "androidx.test.core.internal.os",
+      "androidx.test.core.os",
+      "androidx.test.core.view"
+    ],
+    "androidx.test:monitor:aar": [
+      "androidx.test",
+      "androidx.test.annotation",
+      "androidx.test.internal.platform",
+      "androidx.test.internal.platform.app",
+      "androidx.test.internal.platform.content",
+      "androidx.test.internal.platform.os",
+      "androidx.test.internal.platform.reflect",
+      "androidx.test.internal.platform.util",
+      "androidx.test.internal.runner",
+      "androidx.test.internal.runner.hidden",
+      "androidx.test.internal.runner.intent",
+      "androidx.test.internal.runner.intercepting",
+      "androidx.test.internal.runner.lifecycle",
+      "androidx.test.internal.util",
+      "androidx.test.platform",
+      "androidx.test.platform.app",
+      "androidx.test.platform.concurrent",
+      "androidx.test.platform.device",
+      "androidx.test.platform.graphics",
+      "androidx.test.platform.io",
+      "androidx.test.platform.tracing",
+      "androidx.test.platform.ui",
+      "androidx.test.platform.view.inspector",
+      "androidx.test.runner",
+      "androidx.test.runner.intent",
+      "androidx.test.runner.intercepting",
+      "androidx.test.runner.lifecycle"
+    ],
+    "androidx.tracing:tracing:aar": [
+      "androidx.tracing"
+    ],
+    "androidx.vectordrawable:vectordrawable-animated:aar": [
+      "androidx.vectordrawable.graphics.drawable"
+    ],
+    "androidx.vectordrawable:vectordrawable:aar": [
+      "androidx.vectordrawable.graphics.drawable"
+    ],
+    "androidx.versionedparcelable:versionedparcelable:aar": [
+      "androidx.versionedparcelable"
+    ],
+    "androidx.viewpager:viewpager:aar": [
+      "androidx.viewpager.widget"
+    ],
     "com.almworks.sqlite4java:sqlite4java": [
       "com.almworks.sqlite4java",
       "javolution.util.stripped"
diff --git a/examples/kt_android_local_test/maven_install.json b/examples/kt_android_local_test/maven_install.json
index 2734089..0115c75 100644
--- a/examples/kt_android_local_test/maven_install.json
+++ b/examples/kt_android_local_test/maven_install.json
@@ -1,24 +1,24 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
   "__INPUT_ARTIFACTS_HASH": {
-    "androidx.annotation:annotation": 588494489,
-    "androidx.appcompat:appcompat": -864916242,
-    "androidx.emoji2:emoji2": -667316066,
-    "androidx.emoji2:emoji2-views-helper": -799295894,
-    "androidx.lifecycle:lifecycle-common": 965287242,
-    "androidx.lifecycle:lifecycle-livedata-core": -1534752455,
-    "androidx.lifecycle:lifecycle-runtime": -1133978085,
-    "androidx.lifecycle:lifecycle-viewmodel-savedstate": -1098900718,
-    "androidx.savedstate:savedstate": -833725607,
-    "androidx.test.ext:junit": 923457984,
+    "androidx.annotation:annotation": 1998854284,
+    "androidx.appcompat:appcompat": 526874845,
+    "androidx.emoji2:emoji2": 1270171825,
+    "androidx.emoji2:emoji2-views-helper": -985278619,
+    "androidx.lifecycle:lifecycle-common": -84119015,
+    "androidx.lifecycle:lifecycle-livedata-core": 993085898,
+    "androidx.lifecycle:lifecycle-runtime": 1906619816,
+    "androidx.lifecycle:lifecycle-viewmodel-savedstate": -577646767,
+    "androidx.savedstate:savedstate": -671028048,
+    "androidx.test.ext:junit": 199011195,
     "com.google.code.findbugs:jsr305": 495355163,
-    "com.google.code.gson:gson": 2063056812,
-    "com.google.errorprone:error_prone_annotations": 1790047777,
-    "com.google.guava:guava": -249069712,
-    "com.google.j2objc:j2objc-annotations": -26400317,
-    "junit:junit": -744267592,
-    "org.assertj:assertj-core": -1145412507,
-    "org.robolectric:robolectric": -1784339344,
+    "com.google.code.gson:gson": 579810017,
+    "com.google.errorprone:error_prone_annotations": 1828684942,
+    "com.google.guava:guava": 1001533771,
+    "com.google.j2objc:j2objc-annotations": -57684360,
+    "junit:junit": 1633899997,
+    "org.assertj:assertj-core": -88843862,
+    "org.robolectric:robolectric": 1761992425,
     "repositories": 1670674627
   },
   "__RESOLVED_ARTIFACTS_HASH": {
@@ -1140,13 +1140,40 @@
     ]
   },
   "packages": {
+    "androidx.activity:activity:aar": [
+      "androidx.activity",
+      "androidx.activity.contextaware",
+      "androidx.activity.result",
+      "androidx.activity.result.contract"
+    ],
+    "androidx.annotation:annotation-experimental:aar": [
+      "androidx.annotation",
+      "androidx.annotation.experimental"
+    ],
     "androidx.annotation:annotation-jvm": [
       "androidx.annotation"
     ],
+    "androidx.appcompat:appcompat-resources:aar": [
+      "androidx.appcompat.content.res",
+      "androidx.appcompat.graphics.drawable",
+      "androidx.appcompat.resources",
+      "androidx.appcompat.widget"
+    ],
+    "androidx.appcompat:appcompat:aar": [
+      "androidx.appcompat.app",
+      "androidx.appcompat.graphics.drawable",
+      "androidx.appcompat.text",
+      "androidx.appcompat.view",
+      "androidx.appcompat.view.menu",
+      "androidx.appcompat.widget"
+    ],
     "androidx.arch.core:core-common": [
       "androidx.arch.core.internal",
       "androidx.arch.core.util"
     ],
+    "androidx.arch.core:core-runtime:aar": [
+      "androidx.arch.core.executor"
+    ],
     "androidx.collection:collection-jvm": [
       "androidx.collection",
       "androidx.collection.internal"
@@ -1157,12 +1184,196 @@
     "androidx.concurrent:concurrent-futures-ktx": [
       "androidx.concurrent.futures"
     ],
+    "androidx.core:core-ktx:aar": [
+      "androidx.core.animation",
+      "androidx.core.content",
+      "androidx.core.content.res",
+      "androidx.core.database",
+      "androidx.core.database.sqlite",
+      "androidx.core.graphics",
+      "androidx.core.graphics.drawable",
+      "androidx.core.location",
+      "androidx.core.net",
+      "androidx.core.os",
+      "androidx.core.text",
+      "androidx.core.transition",
+      "androidx.core.util",
+      "androidx.core.view",
+      "androidx.core.widget"
+    ],
+    "androidx.core:core-viewtree:aar": [
+      "androidx.core.viewtree"
+    ],
+    "androidx.core:core:aar": [
+      "android.support.v4.app",
+      "android.support.v4.graphics.drawable",
+      "android.support.v4.os",
+      "androidx.core.accessibilityservice",
+      "androidx.core.app",
+      "androidx.core.app.unusedapprestrictions",
+      "androidx.core.content",
+      "androidx.core.content.pm",
+      "androidx.core.content.res",
+      "androidx.core.database",
+      "androidx.core.database.sqlite",
+      "androidx.core.graphics",
+      "androidx.core.graphics.drawable",
+      "androidx.core.hardware.display",
+      "androidx.core.hardware.fingerprint",
+      "androidx.core.internal.view",
+      "androidx.core.location",
+      "androidx.core.math",
+      "androidx.core.net",
+      "androidx.core.os",
+      "androidx.core.provider",
+      "androidx.core.service.quicksettings",
+      "androidx.core.telephony",
+      "androidx.core.telephony.mbms",
+      "androidx.core.text",
+      "androidx.core.text.method",
+      "androidx.core.text.util",
+      "androidx.core.util",
+      "androidx.core.view",
+      "androidx.core.view.accessibility",
+      "androidx.core.view.animation",
+      "androidx.core.view.autofill",
+      "androidx.core.view.contentcapture",
+      "androidx.core.view.inputmethod",
+      "androidx.core.widget"
+    ],
+    "androidx.cursoradapter:cursoradapter:aar": [
+      "androidx.cursoradapter.widget"
+    ],
+    "androidx.customview:customview:aar": [
+      "androidx.customview.view",
+      "androidx.customview.widget"
+    ],
+    "androidx.drawerlayout:drawerlayout:aar": [
+      "androidx.drawerlayout.widget"
+    ],
+    "androidx.emoji2:emoji2-views-helper:aar": [
+      "androidx.emoji2.viewsintegration"
+    ],
+    "androidx.emoji2:emoji2:aar": [
+      "androidx.emoji2.text",
+      "androidx.emoji2.text.flatbuffer"
+    ],
+    "androidx.fragment:fragment:aar": [
+      "androidx.fragment.app",
+      "androidx.fragment.app.strictmode"
+    ],
+    "androidx.interpolator:interpolator:aar": [
+      "androidx.interpolator.view.animation"
+    ],
     "androidx.lifecycle:lifecycle-common-jvm": [
       "androidx.lifecycle"
     ],
+    "androidx.lifecycle:lifecycle-livedata-core:aar": [
+      "androidx.lifecycle"
+    ],
+    "androidx.lifecycle:lifecycle-livedata:aar": [
+      "androidx.lifecycle"
+    ],
+    "androidx.lifecycle:lifecycle-process:aar": [
+      "androidx.lifecycle"
+    ],
+    "androidx.lifecycle:lifecycle-runtime-android:aar": [
+      "androidx.lifecycle"
+    ],
+    "androidx.lifecycle:lifecycle-viewmodel-android:aar": [
+      "androidx.lifecycle",
+      "androidx.lifecycle.viewmodel",
+      "androidx.lifecycle.viewmodel.internal"
+    ],
+    "androidx.lifecycle:lifecycle-viewmodel-savedstate-android:aar": [
+      "androidx.lifecycle",
+      "androidx.lifecycle.internal",
+      "androidx.lifecycle.serialization"
+    ],
+    "androidx.loader:loader:aar": [
+      "androidx.loader.app",
+      "androidx.loader.content"
+    ],
+    "androidx.profileinstaller:profileinstaller:aar": [
+      "androidx.profileinstaller"
+    ],
     "androidx.resourceinspection:resourceinspection-annotation": [
       "androidx.resourceinspection.annotation"
     ],
+    "androidx.savedstate:savedstate-android:aar": [
+      "androidx.savedstate",
+      "androidx.savedstate.internal",
+      "androidx.savedstate.serialization",
+      "androidx.savedstate.serialization.serializers"
+    ],
+    "androidx.startup:startup-runtime:aar": [
+      "androidx.startup"
+    ],
+    "androidx.test.espresso:espresso-idling-resource:aar": [
+      "androidx.test.espresso",
+      "androidx.test.espresso.idling"
+    ],
+    "androidx.test.ext:junit:aar": [
+      "androidx.test.ext.junit.rules",
+      "androidx.test.ext.junit.runners"
+    ],
+    "androidx.test.services:storage:aar": [
+      "androidx.test.services.storage",
+      "androidx.test.services.storage.file",
+      "androidx.test.services.storage.internal"
+    ],
+    "androidx.test:core:aar": [
+      "androidx.test.core.app",
+      "androidx.test.core.content.pm",
+      "androidx.test.core.graphics",
+      "androidx.test.core.internal.os",
+      "androidx.test.core.os",
+      "androidx.test.core.view"
+    ],
+    "androidx.test:monitor:aar": [
+      "androidx.test",
+      "androidx.test.annotation",
+      "androidx.test.internal.platform",
+      "androidx.test.internal.platform.app",
+      "androidx.test.internal.platform.content",
+      "androidx.test.internal.platform.os",
+      "androidx.test.internal.platform.reflect",
+      "androidx.test.internal.platform.util",
+      "androidx.test.internal.runner",
+      "androidx.test.internal.runner.hidden",
+      "androidx.test.internal.runner.intent",
+      "androidx.test.internal.runner.intercepting",
+      "androidx.test.internal.runner.lifecycle",
+      "androidx.test.internal.util",
+      "androidx.test.platform",
+      "androidx.test.platform.app",
+      "androidx.test.platform.concurrent",
+      "androidx.test.platform.device",
+      "androidx.test.platform.graphics",
+      "androidx.test.platform.io",
+      "androidx.test.platform.tracing",
+      "androidx.test.platform.ui",
+      "androidx.test.platform.view.inspector",
+      "androidx.test.runner",
+      "androidx.test.runner.intent",
+      "androidx.test.runner.intercepting",
+      "androidx.test.runner.lifecycle"
+    ],
+    "androidx.tracing:tracing:aar": [
+      "androidx.tracing"
+    ],
+    "androidx.vectordrawable:vectordrawable-animated:aar": [
+      "androidx.vectordrawable.graphics.drawable"
+    ],
+    "androidx.vectordrawable:vectordrawable:aar": [
+      "androidx.vectordrawable.graphics.drawable"
+    ],
+    "androidx.versionedparcelable:versionedparcelable:aar": [
+      "androidx.versionedparcelable"
+    ],
+    "androidx.viewpager:viewpager:aar": [
+      "androidx.viewpager.widget"
+    ],
     "com.almworks.sqlite4java:sqlite4java": [
       "com.almworks.sqlite4java",
       "javolution.util.stripped"
diff --git a/private/extensions/maven.bzl b/private/extensions/maven.bzl
index 503e6c2..0c3a5ea 100644
--- a/private/extensions/maven.bzl
+++ b/private/extensions/maven.bzl
@@ -114,7 +114,8 @@
             doc = """Policy for user-defined vs. transitive dependency version conflicts
 
             If "pinned", choose the user-specified version in maven_install unconditionally.
-            With the Gradle and Maven resolvers, this only applies to artifacts contributed by the root module.
+            In layered installs, artifacts contributed by the root module take precedence
+            over versions contributed by other modules.
             If "default", follow the selected resolver's default policy.
             """,
             default = "default",
diff --git a/private/lib/layering.bzl b/private/lib/layering.bzl
index 71079a8..eea32ab 100644
--- a/private/lib/layering.bzl
+++ b/private/lib/layering.bzl
@@ -330,7 +330,7 @@
 
 def apply_root_version_conflict_policy(artifacts, resolver, version_conflict_policy):
     """Applies the install-level conflict policy to root module artifacts."""
-    if resolver not in ["gradle", "maven"] or version_conflict_policy != "pinned":
+    if version_conflict_policy != "pinned":
         return artifacts
 
     if resolver == "gradle":
diff --git a/tests/custom_maven_install/coursier_resolved_install.json b/tests/custom_maven_install/coursier_resolved_install.json
index 5ce89d4..7954d08 100644
--- a/tests/custom_maven_install/coursier_resolved_install.json
+++ b/tests/custom_maven_install/coursier_resolved_install.json
@@ -2,7 +2,7 @@
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
   "__INPUT_ARTIFACTS_HASH": {
     "com.google.auth:google-auth-library-oauth2-http": 991267597,
-    "com.google.auto:auto-common": -832702775,
+    "com.google.auto:auto-common": 605821486,
     "com.google.cloud:google-cloud-bigquery": 1661989687,
     "com.google.cloud:libraries-bom": 710163787,
     "repositories": -1949687017
@@ -882,20 +882,12 @@
       "autovalue.shaded.com.google.auto.service",
       "autovalue.shaded.com.google.common.annotations",
       "autovalue.shaded.com.google.common.base",
-      "autovalue.shaded.com.google.common.cache",
       "autovalue.shaded.com.google.common.collect",
-      "autovalue.shaded.com.google.common.escape",
-      "autovalue.shaded.com.google.common.eventbus",
-      "autovalue.shaded.com.google.common.graph",
       "autovalue.shaded.com.google.common.hash",
-      "autovalue.shaded.com.google.common.html",
       "autovalue.shaded.com.google.common.io",
       "autovalue.shaded.com.google.common.math",
-      "autovalue.shaded.com.google.common.net",
       "autovalue.shaded.com.google.common.primitives",
       "autovalue.shaded.com.google.common.reflect",
-      "autovalue.shaded.com.google.common.util.concurrent",
-      "autovalue.shaded.com.google.common.xml",
       "autovalue.shaded.com.google.errorprone.annotations",
       "autovalue.shaded.com.google.errorprone.annotations.concurrent",
       "autovalue.shaded.com.google.escapevelocity",
@@ -1162,8 +1154,6 @@
       "io.netty.util.concurrent",
       "io.netty.util.internal",
       "io.netty.util.internal.logging",
-      "io.netty.util.internal.shaded.org.jctools.counters",
-      "io.netty.util.internal.shaded.org.jctools.maps",
       "io.netty.util.internal.shaded.org.jctools.queues",
       "io.netty.util.internal.shaded.org.jctools.queues.atomic",
       "io.netty.util.internal.shaded.org.jctools.queues.atomic.unpadded",
diff --git a/tests/custom_maven_install/policy_pinned_testing_install.json b/tests/custom_maven_install/policy_pinned_testing_install.json
index 85cf17f..f3a1b13 100644
--- a/tests/custom_maven_install/policy_pinned_testing_install.json
+++ b/tests/custom_maven_install/policy_pinned_testing_install.json
@@ -1,7 +1,48 @@
 {
   "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
-  "__INPUT_ARTIFACTS_HASH": 316608203,
-  "__RESOLVED_ARTIFACTS_HASH": -968606658,
+  "__INPUT_ARTIFACTS_HASH": {
+    "com.google.cloud:google-cloud-storage": -1491579406,
+    "com.google.guava:guava": -1853738824,
+    "repositories": -1949687017
+  },
+  "__RESOLVED_ARTIFACTS_HASH": {
+    "com.fasterxml.jackson.core:jackson-core": -262972278,
+    "com.google.api-client:google-api-client": -793854737,
+    "com.google.api.grpc:proto-google-common-protos": -1885550498,
+    "com.google.api.grpc:proto-google-iam-v1": -453830571,
+    "com.google.api:api-common": -549936361,
+    "com.google.api:gax": 805574868,
+    "com.google.api:gax-httpjson": -1168617842,
+    "com.google.apis:google-api-services-storage": 158985735,
+    "com.google.auth:google-auth-library-credentials": 1709815480,
+    "com.google.auth:google-auth-library-oauth2-http": 735705443,
+    "com.google.cloud:google-cloud-core": -2024787513,
+    "com.google.cloud:google-cloud-core-http": -327232662,
+    "com.google.cloud:google-cloud-storage": -2032350017,
+    "com.google.code.findbugs:jsr305": 870839855,
+    "com.google.code.gson:gson": 1862043778,
+    "com.google.errorprone:error_prone_annotations": 875547987,
+    "com.google.guava:guava": -164902174,
+    "com.google.http-client:google-http-client": -294460505,
+    "com.google.http-client:google-http-client-apache": -921946789,
+    "com.google.http-client:google-http-client-appengine": -849655450,
+    "com.google.http-client:google-http-client-jackson2": -1187124445,
+    "com.google.j2objc:j2objc-annotations": 1702790440,
+    "com.google.oauth-client:google-oauth-client": 1855107119,
+    "com.google.protobuf:protobuf-java": 1939119124,
+    "com.google.protobuf:protobuf-java-util": -944010332,
+    "commons-codec:commons-codec": -1216058892,
+    "commons-logging:commons-logging": 1248790901,
+    "io.grpc:grpc-context": -1228139597,
+    "io.opencensus:opencensus-api": 1930972101,
+    "io.opencensus:opencensus-contrib-http-util": -528120454,
+    "javax.annotation:javax.annotation-api": -1009230154,
+    "org.apache.httpcomponents:httpclient": 483019193,
+    "org.apache.httpcomponents:httpcore": 283803218,
+    "org.checkerframework:checker-compat-qual": -1467964223,
+    "org.codehaus.mojo:animal-sniffer-annotations": -349140135,
+    "org.threeten:threetenbp": 659773227
+  },
   "artifacts": {
     "com.fasterxml.jackson.core:jackson-core": {
       "shasums": {
@@ -672,5 +713,5 @@
       ]
     }
   },
-  "version": "2"
+  "version": "3"
 }
diff --git a/tests/integration/BUILD b/tests/integration/BUILD
index 71654c7..1eac56a 100644
--- a/tests/integration/BUILD
+++ b/tests/integration/BUILD
@@ -179,6 +179,33 @@
     tags = [] if is_bzlmod_enabled() else ["manual"],
 )
 
+genquery(
+    name = "coursier-pinned-layering-deps",
+    expression = "deps(@coursier_pinned_layering//:com_google_code_findbugs_jsr305)",
+    opts = [
+        "--nohost_deps",
+        "--noimplicit_deps",
+    ],
+    scope = ["@coursier_pinned_layering//:com_google_code_findbugs_jsr305"],
+)
+
+genrule(
+    name = "coursier-pinned-layering-deps-sorted",
+    srcs = [":coursier-pinned-layering-deps"],
+    outs = ["coursier-pinned-layering-deps-sorted.txt"],
+    cmd = "cat $< | grep coursier_pinned_layering | sed -e 's|^@@|@|g; s|\\r||g' | sed -e 's|^@[^/]*[+~]|@|g; s|\\r||g' | sort > $@",
+)
+
+# The root pins jsr305 at 3.0.1 while the contributing module offers 3.0.2.
+# Without root precedence under `version_conflict_policy = "pinned"` the
+# resolved version would be 3.0.2.
+diff_test(
+    name = "coursier-pinned-layering-deps-test",
+    file1 = "coursier-pinned-layering-deps.golden.unix",
+    file2 = ":coursier-pinned-layering-deps-sorted.txt",
+    tags = [] if is_bzlmod_enabled() else ["manual"],
+)
+
 # This target will fail to build if we're not handling merging of maven.install
 # tags properly, and if we don't handle multiple lock files properly.
 java_library(
diff --git a/tests/integration/coursier-pinned-layering-deps.golden.unix b/tests/integration/coursier-pinned-layering-deps.golden.unix
new file mode 100644
index 0000000..652243d
--- /dev/null
+++ b/tests/integration/coursier-pinned-layering-deps.golden.unix
@@ -0,0 +1,2 @@
+@coursier_pinned_layering//:com_google_code_findbugs_jsr305
+@coursier_pinned_layering//:v1/https/repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar
diff --git a/tests/integration/coursier_higher_layer/MODULE.bazel b/tests/integration/coursier_higher_layer/MODULE.bazel
index 16f3d13..0e7bfba 100644
--- a/tests/integration/coursier_higher_layer/MODULE.bazel
+++ b/tests/integration/coursier_higher_layer/MODULE.bazel
@@ -14,3 +14,14 @@
     ],
 )
 use_repo(maven, "coursier_layering")
+
+# Contributes a higher version to the `coursier_pinned_layering` repo, whose
+# root pins jsr305 at 3.0.1. The root's `version_conflict_policy = "pinned"`
+# must win over this higher version.
+maven.install(
+    name = "coursier_pinned_layering",
+    artifacts = [
+        "com.google.code.findbugs:jsr305:3.0.2",
+    ],
+)
+use_repo(maven, "coursier_pinned_layering")
diff --git a/tests/unit/layering_test.bzl b/tests/unit/layering_test.bzl
index 597054f..98d6312 100644
--- a/tests/unit/layering_test.bzl
+++ b/tests/unit/layering_test.bzl
@@ -718,6 +718,63 @@
 
 pinned_gradle_root_beats_higher_nonroot_force_test = unittest.make(_pinned_gradle_root_beats_higher_nonroot_force_impl)
 
+def _pinned_coursier_root_beats_higher_nonroot_impl(ctx):
+    env = unittest.begin(ctx)
+    root = _artifact("1.0")
+
+    result = _layer(
+        root_artifacts = [root],
+        resolver = "coursier",
+        version_conflict_policy = "pinned",
+        non_root_artifacts = {"dep": [_artifact("2.0")]},
+    )
+
+    asserts.equals(env, ["1.0"], [artifact.version for artifact in result.artifacts])
+    asserts.true(env, result.artifacts[0].force_version)
+
+    return unittest.end(env)
+
+pinned_coursier_root_beats_higher_nonroot_test = unittest.make(_pinned_coursier_root_beats_higher_nonroot_impl)
+
+def _pinned_coursier_root_beats_higher_nonroot_force_impl(ctx):
+    env = unittest.begin(ctx)
+    root = _artifact("1.0")
+
+    result = _layer(
+        root_artifacts = [root],
+        resolver = "coursier",
+        version_conflict_policy = "pinned",
+        non_root_artifacts = {"dep": [_artifact("2.0", force_version = True)]},
+    )
+
+    asserts.equals(env, ["1.0"], [artifact.version for artifact in result.artifacts])
+    asserts.true(env, result.artifacts[0].force_version)
+
+    return unittest.end(env)
+
+pinned_coursier_root_beats_higher_nonroot_force_test = unittest.make(_pinned_coursier_root_beats_higher_nonroot_force_impl)
+
+def _pinned_coursier_root_wins_without_override_diagnostics_impl(ctx):
+    env = unittest.begin(ctx)
+
+    # `duplicate_version_warning = "error"` must not fail: `pinned` resolves
+    # the displacement deterministically in favour of the root.
+    result = _layer(
+        root_artifacts = [_artifact("1.0")],
+        resolver = "coursier",
+        version_conflict_policy = "pinned",
+        duplicate_version_warning = "error",
+        known_contributing_modules = sets.make(["dep"]),
+        non_root_artifacts = {"dep": [_artifact("2.0")]},
+    )
+
+    asserts.equals(env, ["1.0"], [artifact.version for artifact in result.artifacts])
+    asserts.equals(env, [], result.diagnostics)
+
+    return unittest.end(env)
+
+pinned_coursier_root_wins_without_override_diagnostics_test = unittest.make(_pinned_coursier_root_wins_without_override_diagnostics_impl)
+
 def _namespaces_are_layered_independently_impl(ctx):
     env = unittest.begin(ctx)
 
@@ -918,6 +975,9 @@
         partial.make(boms_merge_with_root_priority_test, size = "small"),
         partial.make(classifier_and_packaging_layer_independently_test, size = "small"),
         partial.make(pinned_gradle_root_beats_higher_nonroot_force_test, size = "small"),
+        partial.make(pinned_coursier_root_beats_higher_nonroot_test, size = "small"),
+        partial.make(pinned_coursier_root_beats_higher_nonroot_force_test, size = "small"),
+        partial.make(pinned_coursier_root_wins_without_override_diagnostics_test, size = "small"),
         partial.make(namespaces_are_layered_independently_test, size = "small"),
         partial.make(diagnostics_preserve_text_gates_and_order_test, size = "small"),
         partial.make(default_namespace_contribution_warning_is_preserved_test, size = "small"),
diff --git a/tests/unit/version_conflict_policy_test.bzl b/tests/unit/version_conflict_policy_test.bzl
index 891a434..1e87a00 100644
--- a/tests/unit/version_conflict_policy_test.bzl
+++ b/tests/unit/version_conflict_policy_test.bzl
@@ -8,7 +8,7 @@
 def _pinned_policy_forces_versioned_root_artifacts_impl(ctx):
     env = unittest.begin(ctx)
 
-    for resolver in ["gradle", "maven"]:
+    for resolver in ["gradle", "maven", "coursier"]:
         versioned = unpack_coordinates("com.example:root:1.0")
         versionless = unpack_coordinates("com.example:managed-by-bom")
 
@@ -30,11 +30,9 @@
 
     artifact = unpack_coordinates("com.example:root:1.0")
 
-    default_artifacts = apply_root_version_conflict_policy([artifact], "gradle", "default")
-    coursier_artifacts = apply_root_version_conflict_policy([artifact], "coursier", "pinned")
-
-    asserts.false(env, hasattr(default_artifacts[0], "force_version"))
-    asserts.false(env, hasattr(coursier_artifacts[0], "force_version"))
+    for resolver in ["gradle", "maven", "coursier"]:
+        default_artifacts = apply_root_version_conflict_policy([artifact], resolver, "default")
+        asserts.false(env, hasattr(default_artifacts[0], "force_version"))
 
     return unittest.end(env)