diff --git a/README.md b/README.md
index 5f3ba41d..c7d5b530 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,7 @@ systems like Buck and Bazel.
6. [Configuration performance](docs/CONFIGURATION-PERFORMANCE.md) — measure + hot-path guidance (F-017 / GH #106)
7. [forma-core public API design](docs/forma-core-api.md) — extraction contract for types / restrictions / registry (F-020)
8. [JVM targets](docs/JVM-TARGETS.md) — pure JVM platform plugin `tools.forma.jvm` (F-030)
+9. [JVM sample application](docs/JVM-SAMPLE.md) — multi-module pure-JVM gold standard (`jvm-application/`, F-031)
Configuration made easy:
@@ -202,6 +203,7 @@ forma-core validation SPI + content rules (Gradle facade in `:validation`): **F-
forma-core TargetRegistry + Android DSL consumers: **F-023** done.
Publish coordinates for `tools.forma:core` (library) + facade deprecation policy: **F-024** done (see [`docs/PLUGIN-PUBLISH.md`](docs/PLUGIN-PUBLISH.md)).
Pure JVM target set (`tools.forma.jvm`, `JvmTargetRegistry`): **F-030** done (see [`docs/JVM-TARGETS.md`](docs/JVM-TARGETS.md)).
+JVM sample application + `binary` composition root: **F-031** done ([`docs/JVM-SAMPLE.md`](docs/JVM-SAMPLE.md), `jvm-application/`).
Icons made by Freepik
from www.flaticon.com
diff --git a/TICKETS.md b/TICKETS.md
index 67021982..b80aa680 100644
--- a/TICKETS.md
+++ b/TICKETS.md
@@ -41,7 +41,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi
| ID | Status | Title | Notes |
|----|--------|-------|-------|
| F-030 | done | JVM target set on forma-core (`library`, `api`, `impl`, `utils`, tests) | `plugins/jvm` + `tools.forma.jvm`; `docs/JVM-TARGETS.md` |
-| F-031 | todo | JVM sample application | |
+| F-031 | done | JVM sample application | `jvm-application/` + `binary` DSL; docs/JVM-SAMPLE.md |
| F-032 | todo | Docs: JVM getting started | |
## P4 — Bazel
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 210aa598..8ec14ba4 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -18,6 +18,7 @@ that compose via `includeBuild`:
|------|------|----------------|-------------------------|
| `plugins/` | Forma Gradle plugins (main product) | 8.3 | Composite + Plugin Portal (`tools.forma.*`) |
| `application/` | Sample Android product (gold standard) | 8.4 | Consumer only |
+| `jvm-application/` | Sample pure-JVM product (`tools.forma.jvm`) | 8.4 | Consumer only |
| `includer/` | Settings plugin: auto-`include` subprojects | (own wrapper) | `tools.forma.includer` |
| `depgen/` | Transitive-deps generation plugin | (own wrapper) | `tools.forma.depgen` (standalone) |
| `build-settings/` | Shared settings conventions (repos) | — | `includeBuild` / convention plugins |
@@ -335,7 +336,7 @@ Suggested extraction order (tickets F-020…F-024):
5. Coordinates + publish finalization: `tools.forma:core` (library) vs `tools.forma.android` + facades (F-024 done — see `docs/PLUGIN-PUBLISH.md`).
6. JVM platform consumer of core — **done (F-030):** `plugins/jvm`, plugin id
`tools.forma.jvm`, `JvmTargetRegistry` + matrix ([`JVM-TARGETS.md`](JVM-TARGETS.md)).
- Sample JVM app is **F-031**.
+ Sample JVM app is **F-031** done (`jvm-application/`, `binary` composition root).
---
@@ -368,7 +369,7 @@ Suggested extraction order (tickets F-020…F-024):
| External deps UX | `plugins/deps` catalog + `build-dependencies` |
| Auto module discovery | `includer/` |
| CI | `.github/workflows/main.yml` |
-| Sample structure | `application/feature/**`, `binary/` · [SAMPLE-APP.md](SAMPLE-APP.md) |
+| Sample structure | `application/feature/**`, `binary/` · [SAMPLE-APP.md](SAMPLE-APP.md); JVM: `jvm-application/` · [JVM-SAMPLE.md](JVM-SAMPLE.md) |
---
diff --git a/docs/JVM-SAMPLE.md b/docs/JVM-SAMPLE.md
new file mode 100644
index 00000000..b097bfc2
--- /dev/null
+++ b/docs/JVM-SAMPLE.md
@@ -0,0 +1,111 @@
+# JVM sample application (F-031)
+
+Pure-JVM product reference for **`tools.forma.jvm`**, parallel to the Android
+[`application/`](../application/) gold standard.
+
+Location: **`jvm-application/`** at the repo root.
+
+## Layout
+
+```
+jvm-application/
+├── binary/ binary (composition root + run)
+├── common/
+│ ├── library/ library
+│ ├── util/ util
+│ └── test-util/ testUtil
+└── feature/
+ ├── greeter/{api,impl}
+ └── calculator/{api,impl}
+```
+
+Includer discovers every `build.gradle.kts` (`arbitraryBuildScriptNames = true`).
+Project path uses `-` instead of `/` for Gradle names (e.g.
+`feature/greeter/impl` → `:feature-greeter-impl`). Forma `target(":feature:greeter:impl")`
+uses colon path form.
+
+## What it exercises
+
+| Target | Role in sample |
+|--------|----------------|
+| `api` / `impl` | Two features (greeter, calculator); **impl ↛ impl** |
+| `library` | Shared math helpers |
+| `util` | String helpers used by greeter impl |
+| `testUtil` | Shared test helpers (wired as `testDependencies` on greeter impl) |
+| `binary` | Composition root: wires both feature **api + impl** + shared modules; Gradle `application` plugin provides `:binary:run` |
+
+Cross-feature code only through `api`. Multiple `impl`s meet only at `binary`.
+
+## Package naming
+
+| Module path | `packageName` |
+|-------------|----------------|
+| `feature/greeter/api` | `tools.forma.jvm.sample.feature.greeter.api` |
+| `feature/greeter/impl` | `tools.forma.jvm.sample.feature.greeter.impl` |
+| `feature/calculator/api` | `tools.forma.jvm.sample.feature.calculator.api` |
+| `feature/calculator/impl` | `tools.forma.jvm.sample.feature.calculator.impl` |
+| `common/library` | `tools.forma.jvm.sample.common.library` |
+| `common/util` | `tools.forma.jvm.sample.common.util` |
+| `common/test-util` | `tools.forma.jvm.sample.common.testutil` |
+| `binary` | `tools.forma.jvm.sample.binary` |
+
+Sources live under `src/main/kotlin//…`.
+
+## Build / run
+
+Requires JDK **17+** (same host bootstrap as Android: `source scripts/env-mac.sh`).
+**No Android SDK.**
+
+```bash
+source scripts/env-mac.sh
+cd jvm-application
+./gradlew build
+./gradlew :binary:run
+```
+
+Expected run output:
+
+```
+Hello, World! (2 + 3 = 5)
+JVM sample (tools.forma.jvm) build + run successful.
+```
+
+Composite includes: `../plugins` (`tools.forma.jvm`), `../includer`,
+`../build-settings` (`convention-dependencies` for `mavenCentral` /
+`google` / Plugin Portal on project resolution).
+
+## Consumer DSL notes
+
+JVM entrypoints live in package **`tools.forma.jvm`** (unlike Android DSL files
+in the default package). Sample scripts import them explicitly:
+
+```kotlin
+import tools.forma.jvm.api
+import tools.forma.jvm.impl
+import tools.forma.jvm.binary
+// …
+
+binary(
+ packageName = "tools.forma.jvm.sample.binary",
+ mainClass = "tools.forma.jvm.sample.binary.MainKt",
+ dependencies = deps(
+ target(":feature:greeter:api"),
+ target(":feature:greeter:impl"),
+ // …
+ )
+)
+```
+
+For a top-level `fun main()` in `Main.kt`, `mainClass` is `…MainKt`.
+
+## Matrix
+
+Full pure-JVM dependency matrix (including `binary`):
+[`JVM-TARGETS.md`](JVM-TARGETS.md).
+
+## See also
+
+- [`JVM-TARGETS.md`](JVM-TARGETS.md) — types + matrix
+- [`SAMPLE-APP.md`](SAMPLE-APP.md) — Android multi-feature analog
+- [`VISION.md`](VISION.md) — product sequencing (JVM after Android + core)
+- F-032 — fuller JVM getting-started tutorial (next)
diff --git a/docs/JVM-TARGETS.md b/docs/JVM-TARGETS.md
index 1467daa7..4b6bd4b3 100644
--- a/docs/JVM-TARGETS.md
+++ b/docs/JVM-TARGETS.md
@@ -1,4 +1,4 @@
-# JVM targets (F-030)
+# JVM targets (F-030 / F-031)
Pure **JVM** platform for Forma, built on **forma-core** (`tools.forma:core`)
without AGP. Parallel to the Android product (`tools.forma.android`), not a
@@ -15,37 +15,38 @@ Plugin id: **`tools.forma.jvm`** (module `plugins/:jvm`).
| `library` | `jvm.library` | `library` | Shared pure-JVM library |
| `util` | `jvm.util` | `util` | Utilities / extensions |
| `testUtil` | `jvm.test-util` | `test-util` | Shared test helpers |
+| `binary` | `jvm.binary` | `binary` | Composition root + runnable app |
Name matching is the same as Android / core: project name equals the suffix or
ends with `-$suffix` (see `SuffixNameMatcher`).
-Composition roots (`binary` / app) are **out of scope for F-030** (sample app
-is F-031). Until then, feature graphs should avoid needing multi-`impl`
-composition at a root.
+`binary(...)` applies `org.jetbrains.kotlin.jvm` + Gradle `application`, sets
+`mainClass`, and may depend on multiple `impl` modules (composition root).
## Dependency matrix (project edges only)
External / catalog GAV deps are not filtered by type (same as Android).
-| Consumer ↓ \ Dep → | api | impl | library | util | test-util |
-|--------------------|-----|------|---------|------|-----------|
-| **api** | Y | — | Y | — | — |
-| **impl** | Y | — | Y | Y | Y |
-| **library** | — | — | — | Y | Y |
-| **util** | — | — | Y | Y | — |
-| **test-util** | — | — | Y | Y | Y |
+| Consumer ↓ \ Dep → | api | impl | library | util | test-util | binary |
+|--------------------|-----|------|---------|------|-----------|--------|
+| **api** | Y | — | Y | — | — | — |
+| **impl** | Y | — | Y | Y | Y | — |
+| **library** | — | — | — | Y | Y | — |
+| **util** | — | — | Y | Y | — | — |
+| **test-util** | — | — | Y | Y | Y | — |
+| **binary** | Y | Y | Y | Y | Y | — |
### Rules of thumb
-- **`impl` ↛ `impl`** — implementations stay independent; compose later at a
- binary/app root (F-031).
+- **`impl` ↛ `impl`** — implementations stay independent; compose at `binary`.
- **`api` only sees contracts** — `api` + `library` (no util/impl leakage into
the public surface).
- **`library` stays leaf-ish** — only `util` / `test-util` (parity with the
historical JVM `library()` row inside the Android matrix).
- **`util` cannot depend on `api` / `impl`**.
+- **`binary` is the composition root** — may pull api + multiple impls + shared.
- **Content rules** — pure JVM has no Android `res/`; no content rules are
- attached for F-030.
+ attached.
Source of truth in code: `plugins/jvm/.../JvmTargetRegistry.kt`
(`registerJvmDefaults`).
@@ -56,8 +57,9 @@ Source of truth in code: `plugins/jvm/.../JvmTargetRegistry.kt`
`testUtil`) **inside** `tools.forma.android`, registered on
`AndroidTargetRegistry` with some shared **id strings** (`jvm.library`,
`jvm.util`).
-- F-030 introduces an **authoritative JVM platform registry**
+- F-030 introduced an **authoritative JVM platform registry**
(`JvmTargetRegistry`) and plugin so pure-JVM products do not need AGP.
+- F-031 added **`binary`** + the multi-module sample `jvm-application/`.
- Registries are **separate singletons**; shared id strings are intentional.
- Full Android matrix remains [`DEPENDENCY-MATRIX.md`](DEPENDENCY-MATRIX.md).
@@ -73,12 +75,12 @@ pluginManagement {
}
plugins {
id("tools.forma.jvm") version "0.1.3"
+ // or includeBuild("../plugins") in a composite
}
-// some-module/build.gradle.kts
+// module build.gradle.kts
import tools.forma.jvm.api
import tools.forma.jvm.library
-// …
api(
packageName = "com.example.feature.api",
@@ -98,17 +100,18 @@ compatibility is Java **11** (`JvmDefaults`).
## Tests
- `plugins/jvm` unit tests: `JvmTargetRegistryTest` (matrix allow/deny,
- self-suffix, validator identity cache).
+ self-suffix, validator identity cache, binary composition-root row).
- Engine coverage remains in `plugins/core` tests.
+- Integration: [`jvm-application/`](../jvm-application/) (`./gradlew build` /
+ `:binary:run`).
## Next
-- **F-031** — multi-module JVM sample application (gold standard like
- `application/`).
-- **F-032** — JVM getting-started tutorial.
+- **F-032** — JVM getting-started tutorial (point at this doc + sample).
## See also
+- [`JVM-SAMPLE.md`](JVM-SAMPLE.md) — multi-module sample layout + run
- [`forma-core-api.md`](forma-core-api.md) — core types / registry SPI
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — module graph
- [`PLUGIN-PUBLISH.md`](PLUGIN-PUBLISH.md) — coordinates
diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md
index 99893443..1826af4f 100644
--- a/docs/PROGRESS.md
+++ b/docs/PROGRESS.md
@@ -2,6 +2,31 @@
Newest entries first.
+## 2026-07-13 — F-031 JVM sample application
+
+- **Ticket:** F-031 → `done`
+- **Branch:** `forma/F-031-jvm-sample` (from `origin/v2`)
+- **Skills/modes:** Grok Build `--mode full` (design plan completed; implement hit max-turns with partial tree on disk); Hermes finished wiring, verify, docs, commit/PR
+- **Platform (`plugins/jvm`):**
+ - New type **`jvm.binary`** + `binary(...)` DSL (Kotlin JVM + Gradle `application` + `mainClass`)
+ - `registerJvmDefaults` composition-root row: binary → api/impl/library/util/test-util (no binary→binary)
+ - `impl` / `library` `testDependencies` widened to `FormaDependency` (project test-util)
+ - `JvmTargetRegistryTest` updated for six types + binary matrix/self-suffix
+- **Sample (`jvm-application/`):**
+ - Multi-module pure-JVM product: greeter + calculator features (api/impl), common library/util/test-util, binary composition root
+ - Composite: `includeBuild` plugins + includer + build-settings; `convention-dependencies` for project repos
+ - Explicit `import tools.forma.jvm.*` in module scripts (JVM DSL is packaged, not default package)
+ - Runnable: `./gradlew :binary:run` → `Hello, World! (2 + 3 = 5)`
+- **Docs:** `docs/JVM-SAMPLE.md`; JVM-TARGETS matrix + binary row; ARCHITECTURE layout; README Progress + getting-started link; TICKETS
+- **Verify (OpenJDK 17 + env-mac.sh):**
+ - `plugins/`: `./gradlew :core:test :jvm:test build` → **BUILD SUCCESSFUL** (earlier full build; re-run `:jvm:test` green)
+ - `jvm-application/`: `./gradlew build :binary:run` → **BUILD SUCCESSFUL** (run output as above)
+ - `application/`: `./gradlew :binary:assembleDebug` → **BUILD SUCCESSFUL** (578 tasks)
+- **Grounded:** all commands executed; no invented green builds.
+- **Commits/PRs:** this run — push + PR base `v2`
+- **Blockers:** none (Grok implement max-turns; completed under Hermes)
+- **Next step:** F-032 Docs: JVM getting started
+
## 2026-07-13 — F-030 JVM target set on forma-core
- **Ticket:** F-030 → `done`
diff --git a/jvm-application/binary/build.gradle.kts b/jvm-application/binary/build.gradle.kts
new file mode 100644
index 00000000..41cdae9e
--- /dev/null
+++ b/jvm-application/binary/build.gradle.kts
@@ -0,0 +1,14 @@
+import tools.forma.jvm.binary
+
+binary(
+ packageName = "tools.forma.jvm.sample.binary",
+ mainClass = "tools.forma.jvm.sample.binary.MainKt",
+ dependencies = deps(
+ target(":feature:greeter:api"),
+ target(":feature:greeter:impl"),
+ target(":feature:calculator:api"),
+ target(":feature:calculator:impl"),
+ target(":common:util"),
+ target(":common:library")
+ )
+)
diff --git a/jvm-application/binary/src/main/kotlin/tools/forma/jvm/sample/binary/Main.kt b/jvm-application/binary/src/main/kotlin/tools/forma/jvm/sample/binary/Main.kt
new file mode 100644
index 00000000..87601a85
--- /dev/null
+++ b/jvm-application/binary/src/main/kotlin/tools/forma/jvm/sample/binary/Main.kt
@@ -0,0 +1,22 @@
+package tools.forma.jvm.sample.binary
+
+import tools.forma.jvm.sample.feature.greeter.api.Greeter
+import tools.forma.jvm.sample.feature.greeter.impl.GreeterImpl
+import tools.forma.jvm.sample.feature.calculator.api.Calculator
+import tools.forma.jvm.sample.feature.calculator.impl.CalculatorImpl
+
+/**
+ * Composition root demo for F-031.
+ * Wires impls (greeter + calculator) + shared library/util only at the binary.
+ * Cross-feature communication happens exclusively through api modules.
+ */
+fun main() {
+ val greeter: Greeter = GreeterImpl()
+ val calculator: Calculator = CalculatorImpl()
+
+ val greeting = greeter.greet("world")
+ val sum = calculator.add(2, 3)
+
+ println("$greeting (2 + 3 = $sum)")
+ println("JVM sample (tools.forma.jvm) build + run successful.")
+}
diff --git a/jvm-application/build.gradle.kts b/jvm-application/build.gradle.kts
new file mode 100644
index 00000000..55a2a9df
--- /dev/null
+++ b/jvm-application/build.gradle.kts
@@ -0,0 +1,4 @@
+// Root build for the pure-JVM Forma sample (F-031).
+// No androidProjectConfiguration or jvmProjectConfiguration hook is required yet.
+// The tools.forma.jvm settings plugin + DSL entrypoints (api/impl/library/util/testUtil/binary)
+// configure each module when their build.gradle.kts are evaluated.
diff --git a/jvm-application/common/library/build.gradle.kts b/jvm-application/common/library/build.gradle.kts
new file mode 100644
index 00000000..812f4b21
--- /dev/null
+++ b/jvm-application/common/library/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.library
+
+library(packageName = "tools.forma.jvm.sample.common.library")
diff --git a/jvm-application/common/library/src/main/kotlin/tools/forma/jvm/sample/common/library/Math.kt b/jvm-application/common/library/src/main/kotlin/tools/forma/jvm/sample/common/library/Math.kt
new file mode 100644
index 00000000..d9d4da55
--- /dev/null
+++ b/jvm-application/common/library/src/main/kotlin/tools/forma/jvm/sample/common/library/Math.kt
@@ -0,0 +1,6 @@
+package tools.forma.jvm.sample.common.library
+
+object Math {
+ fun add(a: Int, b: Int): Int = a + b
+ fun multiply(a: Int, b: Int): Int = a * b
+}
diff --git a/jvm-application/common/test-util/build.gradle.kts b/jvm-application/common/test-util/build.gradle.kts
new file mode 100644
index 00000000..a3c4d5a2
--- /dev/null
+++ b/jvm-application/common/test-util/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.testUtil
+
+testUtil(packageName = "tools.forma.jvm.sample.common.testutil")
diff --git a/jvm-application/common/test-util/src/main/kotlin/tools/forma/jvm/sample/common/testutil/TestHelpers.kt b/jvm-application/common/test-util/src/main/kotlin/tools/forma/jvm/sample/common/testutil/TestHelpers.kt
new file mode 100644
index 00000000..371aa333
--- /dev/null
+++ b/jvm-application/common/test-util/src/main/kotlin/tools/forma/jvm/sample/common/testutil/TestHelpers.kt
@@ -0,0 +1,8 @@
+package tools.forma.jvm.sample.common.testutil
+
+// Shared test helpers (exercises jvm.test-util target + matrix).
+// No src/test sources in this minimal sample; the module is declared via testDependencies
+// on impls if needed in future. Presence proves registration and build.
+object TestHelpers {
+ fun echo(s: String): String = s
+}
diff --git a/jvm-application/common/util/build.gradle.kts b/jvm-application/common/util/build.gradle.kts
new file mode 100644
index 00000000..f700d354
--- /dev/null
+++ b/jvm-application/common/util/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.util
+
+util(packageName = "tools.forma.jvm.sample.common.util")
diff --git a/jvm-application/common/util/src/main/kotlin/tools/forma/jvm/sample/common/util/Strings.kt b/jvm-application/common/util/src/main/kotlin/tools/forma/jvm/sample/common/util/Strings.kt
new file mode 100644
index 00000000..c4be1980
--- /dev/null
+++ b/jvm-application/common/util/src/main/kotlin/tools/forma/jvm/sample/common/util/Strings.kt
@@ -0,0 +1,3 @@
+package tools.forma.jvm.sample.common.util
+
+fun greet(name: String): String = "Hello, ${name.lowercase().replaceFirstChar { it.uppercase() }}!"
diff --git a/jvm-application/feature/calculator/api/build.gradle.kts b/jvm-application/feature/calculator/api/build.gradle.kts
new file mode 100644
index 00000000..398577f9
--- /dev/null
+++ b/jvm-application/feature/calculator/api/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.api
+
+api(packageName = "tools.forma.jvm.sample.feature.calculator.api")
diff --git a/jvm-application/feature/calculator/api/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/api/Calculator.kt b/jvm-application/feature/calculator/api/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/api/Calculator.kt
new file mode 100644
index 00000000..cd1282b4
--- /dev/null
+++ b/jvm-application/feature/calculator/api/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/api/Calculator.kt
@@ -0,0 +1,5 @@
+package tools.forma.jvm.sample.feature.calculator.api
+
+interface Calculator {
+ fun add(a: Int, b: Int): Int
+}
diff --git a/jvm-application/feature/calculator/impl/build.gradle.kts b/jvm-application/feature/calculator/impl/build.gradle.kts
new file mode 100644
index 00000000..410a3437
--- /dev/null
+++ b/jvm-application/feature/calculator/impl/build.gradle.kts
@@ -0,0 +1,10 @@
+import tools.forma.jvm.impl
+
+impl(
+ packageName = "tools.forma.jvm.sample.feature.calculator.impl",
+ dependencies = deps(
+ target(":feature:calculator:api"),
+ target(":common:library"),
+ target(":common:util")
+ )
+)
diff --git a/jvm-application/feature/calculator/impl/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/impl/CalculatorImpl.kt b/jvm-application/feature/calculator/impl/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/impl/CalculatorImpl.kt
new file mode 100644
index 00000000..aef5e66c
--- /dev/null
+++ b/jvm-application/feature/calculator/impl/src/main/kotlin/tools/forma/jvm/sample/feature/calculator/impl/CalculatorImpl.kt
@@ -0,0 +1,8 @@
+package tools.forma.jvm.sample.feature.calculator.impl
+
+import tools.forma.jvm.sample.common.library.Math
+import tools.forma.jvm.sample.feature.calculator.api.Calculator
+
+class CalculatorImpl : Calculator {
+ override fun add(a: Int, b: Int): Int = Math.add(a, b)
+}
diff --git a/jvm-application/feature/greeter/api/build.gradle.kts b/jvm-application/feature/greeter/api/build.gradle.kts
new file mode 100644
index 00000000..a5c37dfb
--- /dev/null
+++ b/jvm-application/feature/greeter/api/build.gradle.kts
@@ -0,0 +1,3 @@
+import tools.forma.jvm.api
+
+api(packageName = "tools.forma.jvm.sample.feature.greeter.api")
diff --git a/jvm-application/feature/greeter/api/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/api/Greeter.kt b/jvm-application/feature/greeter/api/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/api/Greeter.kt
new file mode 100644
index 00000000..be30467a
--- /dev/null
+++ b/jvm-application/feature/greeter/api/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/api/Greeter.kt
@@ -0,0 +1,5 @@
+package tools.forma.jvm.sample.feature.greeter.api
+
+interface Greeter {
+ fun greet(name: String): String
+}
diff --git a/jvm-application/feature/greeter/impl/build.gradle.kts b/jvm-application/feature/greeter/impl/build.gradle.kts
new file mode 100644
index 00000000..30f09556
--- /dev/null
+++ b/jvm-application/feature/greeter/impl/build.gradle.kts
@@ -0,0 +1,13 @@
+import tools.forma.jvm.impl
+
+impl(
+ packageName = "tools.forma.jvm.sample.feature.greeter.impl",
+ dependencies = deps(
+ target(":feature:greeter:api"),
+ target(":common:util"),
+ target(":common:library")
+ ),
+ testDependencies = deps(
+ target(":common:test-util")
+ )
+)
diff --git a/jvm-application/feature/greeter/impl/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/impl/GreeterImpl.kt b/jvm-application/feature/greeter/impl/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/impl/GreeterImpl.kt
new file mode 100644
index 00000000..1e400ae3
--- /dev/null
+++ b/jvm-application/feature/greeter/impl/src/main/kotlin/tools/forma/jvm/sample/feature/greeter/impl/GreeterImpl.kt
@@ -0,0 +1,8 @@
+package tools.forma.jvm.sample.feature.greeter.impl
+
+import tools.forma.jvm.sample.common.util.greet as formatGreeting
+import tools.forma.jvm.sample.feature.greeter.api.Greeter
+
+class GreeterImpl : Greeter {
+ override fun greet(name: String): String = formatGreeting(name)
+}
diff --git a/jvm-application/gradle.properties b/jvm-application/gradle.properties
new file mode 100644
index 00000000..dfe21152
--- /dev/null
+++ b/jvm-application/gradle.properties
@@ -0,0 +1,6 @@
+# Pure-JVM Forma sample (F-031) — no Android keys.
+org.gradle.jvmargs=-Xmx2048m -XX:+UseParallelGC
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
+kotlin.code.style=official
diff --git a/jvm-application/gradle/wrapper/gradle-wrapper.jar b/jvm-application/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7f93135c
Binary files /dev/null and b/jvm-application/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/jvm-application/gradle/wrapper/gradle-wrapper.properties b/jvm-application/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3fa8f862
--- /dev/null
+++ b/jvm-application/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/jvm-application/gradlew b/jvm-application/gradlew
new file mode 100755
index 00000000..0adc8e1a
--- /dev/null
+++ b/jvm-application/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/jvm-application/gradlew.bat b/jvm-application/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/jvm-application/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/jvm-application/settings.gradle.kts b/jvm-application/settings.gradle.kts
new file mode 100644
index 00000000..10dc4e6a
--- /dev/null
+++ b/jvm-application/settings.gradle.kts
@@ -0,0 +1,20 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ mavenCentral()
+ }
+ apply(from = "../build-settings/conventions/src/main/kotlin/convention-plugins.settings.gradle.kts")
+ includeBuild("../build-settings")
+ includeBuild("../plugins")
+ includeBuild("../includer")
+}
+
+plugins {
+ id("convention-dependencies")
+ id("tools.forma.includer")
+ id("tools.forma.jvm")
+}
+
+includer { arbitraryBuildScriptNames = true }
+
+rootProject.name = "jvm-application"
diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/binary.kt b/plugins/jvm/src/main/java/tools/forma/jvm/binary.kt
new file mode 100644
index 00000000..2354c917
--- /dev/null
+++ b/plugins/jvm/src/main/java/tools/forma/jvm/binary.kt
@@ -0,0 +1,50 @@
+package tools.forma.jvm
+
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaApplication
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.configure
+import tools.forma.deps.core.EmptyDependency
+import tools.forma.deps.core.FormaDependency
+import tools.forma.deps.core.applyDependencies
+import tools.forma.jvm.feature.applyKotlinJvm
+import tools.forma.jvm.target.JvmTargetRegistry
+import tools.forma.jvm.target.JvmTargetTypes
+import tools.forma.jvm.target.registerJvmDefaults
+import tools.forma.owners.NoOwner
+import tools.forma.owners.Owner
+import tools.forma.target.FormaTarget
+import tools.forma.validation.asValidator
+
+/**
+ * Pure JVM **binary** (composition root + runnable application).
+ *
+ * - Wires multiple `impl` modules (and api/library/util) at a single root.
+ * - Applies `org.jetbrains.kotlin.jvm` + `application` (enables `./gradlew :binary:run`).
+ * - `mainClass` must be the Kotlin `...MainKt` for a top-level `fun main()` in `Main.kt`.
+ *
+ * Matrix: binary may depend on api, impl, library, util, test-util.
+ */
+fun Project.binary(
+ packageName: String,
+ mainClass: String,
+ owner: Owner = NoOwner,
+ dependencies: FormaDependency = EmptyDependency
+) {
+ registerJvmDefaults()
+
+ JvmTargetRegistry.selfValidator(JvmTargetTypes.binary).asValidator()
+ .validate(FormaTarget(this))
+
+ applyKotlinJvm()
+ apply(plugin = "application")
+
+ configure {
+ this.mainClass.set(mainClass)
+ }
+
+ applyDependencies(
+ validator = JvmTargetRegistry.validatorFor(JvmTargetTypes.binary).asValidator(),
+ dependencies = dependencies
+ )
+}
diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt b/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt
index 4410962b..34430bd3 100644
--- a/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt
+++ b/plugins/jvm/src/main/java/tools/forma/jvm/impl.kt
@@ -3,7 +3,6 @@ package tools.forma.jvm
import org.gradle.api.Project
import tools.forma.deps.core.EmptyDependency
import tools.forma.deps.core.FormaDependency
-import tools.forma.deps.core.NamedDependency
import tools.forma.deps.core.applyDependencies
import tools.forma.jvm.feature.applyKotlinJvm
import tools.forma.jvm.target.JvmTargetRegistry
@@ -18,12 +17,12 @@ import tools.forma.validation.asValidator
* Dagger2-friendly boundaries:
* - May depend on `api`, `library`, `util`, `test-util`.
* - **Must not** depend on other `impl` modules.
- * Composition of impls happens at a binary / app root (later F-031).
+ * Composition of impls happens at a binary root (`binary(...)`, F-031).
*/
fun Project.impl(
packageName: String,
dependencies: FormaDependency = EmptyDependency,
- testDependencies: NamedDependency = NamedDependency()
+ testDependencies: FormaDependency = EmptyDependency
) {
registerJvmDefaults()
diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/library.kt b/plugins/jvm/src/main/java/tools/forma/jvm/library.kt
index 0bb9ac76..8f8ad6be 100644
--- a/plugins/jvm/src/main/java/tools/forma/jvm/library.kt
+++ b/plugins/jvm/src/main/java/tools/forma/jvm/library.kt
@@ -3,7 +3,6 @@ package tools.forma.jvm
import org.gradle.api.Project
import tools.forma.deps.core.EmptyDependency
import tools.forma.deps.core.FormaDependency
-import tools.forma.deps.core.NamedDependency
import tools.forma.deps.core.applyDependencies
import tools.forma.jvm.feature.applyKotlinJvm
import tools.forma.jvm.target.JvmTargetRegistry
@@ -23,7 +22,7 @@ fun Project.library(
packageName: String,
dependencies: FormaDependency = EmptyDependency,
owner: Owner = NoOwner,
- testDependencies: NamedDependency = NamedDependency()
+ testDependencies: FormaDependency = EmptyDependency
) {
registerJvmDefaults()
diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt
index a46fec83..d91ae05d 100644
--- a/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt
+++ b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetRegistry.kt
@@ -20,6 +20,7 @@ object JvmTargetRegistry : TargetRegistry by DefaultTargetRegistry()
* - library → util, test-util
* - util → util, library
* - testUtil → test-util, util (library allowed for test helpers that need prod contracts)
+ * - binary → api, impl, library, util, test-util (composition root for wiring multiple impls)
*
* No ContentRules attached for pure JVM (no Android resources concept).
* Safe to call multiple times (re-register replaces).
@@ -66,4 +67,12 @@ fun registerJvmDefaults(registry: TargetRegistry = JvmTargetRegistry) {
allowedDependencies = setOf(t.testUtil, t.util, t.library)
)
)
+
+ // binary (composition root): wires api + multiple impls + shared library/util for a runnable JVM app
+ registry.register(
+ TargetRegistration(
+ type = t.binary,
+ allowedDependencies = setOf(t.api, t.impl, t.library, t.util, t.testUtil)
+ )
+ )
}
diff --git a/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt
index 27133fca..1fef230c 100644
--- a/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt
+++ b/plugins/jvm/src/main/java/tools/forma/jvm/target/JvmTargetTypes.kt
@@ -26,4 +26,7 @@ object JvmTargetTypes {
/** Test helpers (JVM). */
val testUtil: TargetType = targetType("jvm.test-util", "test-util")
+
+ /** JVM binary / composition root (runnable app entrypoint). */
+ val binary: TargetType = targetType("jvm.binary", "binary")
}
diff --git a/plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt b/plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt
index 2533a0ca..95d44bf4 100644
--- a/plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt
+++ b/plugins/jvm/src/test/java/tools/forma/jvm/target/JvmTargetRegistryTest.kt
@@ -24,14 +24,15 @@ class JvmTargetRegistryTest {
}
@Test
- fun `all five jvm types are registered by id`() {
+ fun `all six jvm types are registered by id`() {
val t = JvmTargetTypes
assertEquals(t.api, JvmTargetRegistry.get("jvm.api"))
assertEquals(t.impl, JvmTargetRegistry.get("jvm.impl"))
assertEquals(t.library, JvmTargetRegistry.get("jvm.library"))
assertEquals(t.util, JvmTargetRegistry.get("jvm.util"))
assertEquals(t.testUtil, JvmTargetRegistry.get("jvm.test-util"))
- assertEquals(5, JvmTargetRegistry.all().size)
+ assertEquals(t.binary, JvmTargetRegistry.get("jvm.binary"))
+ assertEquals(6, JvmTargetRegistry.all().size)
}
@Test
@@ -71,6 +72,14 @@ class JvmTargetRegistryTest {
assertTrue(g.isAllowed(t.testUtil, t.library))
assertFalse(g.isAllowed(t.testUtil, t.api))
assertFalse(g.isAllowed(t.testUtil, t.impl))
+
+ // binary (composition root): may consume impls + api + shared; no binary→binary
+ assertTrue(g.isAllowed(t.binary, t.api))
+ assertTrue(g.isAllowed(t.binary, t.impl))
+ assertTrue(g.isAllowed(t.binary, t.library))
+ assertTrue(g.isAllowed(t.binary, t.util))
+ assertTrue(g.isAllowed(t.binary, t.testUtil))
+ assertFalse(g.isAllowed(t.binary, t.binary))
}
@Test
@@ -103,6 +112,8 @@ class JvmTargetRegistryTest {
JvmTargetRegistry.selfValidator(t.library).validate(TestRef("common-library"))
JvmTargetRegistry.selfValidator(t.util).validate(TestRef("network-util"))
JvmTargetRegistry.selfValidator(t.testUtil).validate(TestRef("shared-test-util"))
+ JvmTargetRegistry.selfValidator(t.binary).validate(TestRef("binary"))
+ JvmTargetRegistry.selfValidator(t.binary).validate(TestRef("app-binary"))
}
@Test