dependencySubstitution not working with composite build with multiple levels of includeBuild

I have a Gradle 8.5 project with a top-level settings.gradle.kts includes sub-project1/settings.gradle.kts and sub-project2/settings.gradle.kts, each of which, in turn, includes their own sub-sub-project. Each of these sub-sub-project sub-projects is actually a symlink to ../sub-sub-project, which is also included by the root level project.

Both sub-project1 and sub-project2 build fine on their own, but when trying to run gradle build on the top-level project, I get the (not unexpected) error:

Could not determine the dependencies of task ':ModuleA:compileJava'.
> Could not resolve all task dependencies for configuration ':ModuleA:compileClasspath'.
   > Could not resolve sub-sub-project:ModuleX.
     Required by:
         project :ModuleA > project :sub-project1:ModuleB
         project :ModuleA > project :sub-project2:ModuleC
      > Module version 'sub-sub-project:ModuleX' is not unique in composite: can be provided by [project :sub-project1:sub-sub-project:ModuleX, project :sub-project2:sub-sub-project:ModuleX, project :sub-sub-project:ModuleX].

This seems to clearly be a job for dependencySubstitution. However, when I try to add it to the top-level settings.gradle.kts like so:

includeBuild("sub-sub-project")

includeBuild("sub-project1") {
    dependencySubstitution {
        substitute(module("sub-sub-project:ModuleX")).using(project(":sub-sub-project"))
    }
}

includeBuild("sub-project2") {
    dependencySubstitution {
        substitute(module("sub-sub-project:ModuleX")).using(project(":sub-sub-project"))
    }
}

include(":ModuleA")

I get the error:

Project with path ':sub-sub-project' not found in build ':sub-project1'.

The sub-projects (which I cannot alter in the real-world more complex version of this scenario) each look like:

// ModuleA/build.gradle.kts
dependencies {
    api("sub-project1:ModuleB")
    api("sub-project2:ModuleC")
}
// sub-project1/settings.gradle.kts
includeBuild("sub-sub-project")
include(":ModuleB")
// sub-project1/ModuleB/build.gradle.kts
dependencies {
    api("sub-sub-project:ModuleX")
}
// sub-project2/settings.gradle.kts
includeBuild("sub-sub-project")
include(":ModuleC")
// sub-project2/ModuleC/build.gradle.kts
dependencies {
    api("sub-sub-project:ModuleX")
}
// sub-sub-project/settings.gradle.kts
include(":ModuleX")

Is there some solution to this build failure that can be applied to the top-level settings.gradle.kts?

I’ve pushed the demo project to GitHub - marcprux/gradle-module-demo. My goal is to get gradle build working on a fresh checkout of that repo. Can anyone help?

The project structure looks like this:

tree -I build --filesfirst 
gradle-module-demo
├── settings.gradle.kts
├── ModuleA
│   ├── build.gradle.kts
│   └── src
│       └── main
│           └── kotlin
│               └── ModuleA.kt
├── sub-project1
│   ├── settings.gradle.kts
│   ├── ModuleB
│   │   ├── build.gradle.kts
│   │   └── src
│   │       └── main
│   │           └── kotlin
│   │               └── ModuleB.kt
│   └── sub-sub-project -> ../sub-sub-project
├── sub-project2
│   ├── settings.gradle.kts
│   ├── ModuleC
│   │   ├── build.gradle.kts
│   │   └── src
│   │       └── main
│   │           └── kotlin
│   │               └── ModuleC.kt
│   └── sub-sub-project -> ../sub-sub-project
└── sub-sub-project
    ├── settings.gradle.kts
    └── ModuleX
        ├── build.gradle.kts
        └── src
            └── main
                └── kotlin
                    └── ModuleX.kt

How strict is “which I cannot alter in the real-world more complex version of this scenario”?
Can’t you remove those non-sense non-portable symlinks and instead do includeBuild("../sub-sub-project") in the sub projects? Then it is the same build that is included, there is no conflict and it also does not need to be built twice or three times.

They aren’t really symlinks in the real worlds scenario – I just made them so to demonstrate that the contents of sub-sub-project are all the same. In the real-world scenario, they are all separate sub projects whose contents should be substituted with the parent’s sub-sub-project.

Hm, I see.
But I don’t think this is possible or even makes sense.

Dependency substitution definitely is not the solution, because it is the other way around.
If you have in root project

includeBuild("sub-project1") {
    dependencySubstitution {
        substitute(module("sub-sub-project:ModuleX")).using(project(":sub-sub-project"))
    }
}

that means that if root project wants to depend on sub-sub-project:ModuleX, this should be substituted using project(":sub-sub-project") from sub-project1.

Maybe someone else comes up with a solution, but I don’t see one right now, especially not without modifying sub-projectX.