Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ project-dep type checks + content layout rules from live validators
| `library` | ✅ | JVM library | project-dep list (only owner of `library` suffix after F-063) |
| `api` | ✅ | Feature external APIs | no `res/` + project-dep list |
| `impl` | ✅ | Feature implementation | project-dep list (**no** other `impl`) |
| `androidNative` | ✅ | NDK / native | no `res/`; no project-dep validation yet |
| `androidNative` | ✅ | NDK / native | no `res/`; leaf (no first-party deps); consumers: `androidUtil` / `app` / `binary` |

## Dependency matrix

Expand All @@ -192,16 +192,16 @@ never a per-call-site escape hatch. Summary:
| `library` (JVM) | `util`, `test-util` |
| `uiLibrary` | `widget`, `compose-widget`, `util`, `android-util`, `res` |
| `util` | `util`, `library` |
| `androidUtil` | `android-util`, `test-util`, `res`, `library` |
| `androidUtil` | `android-util`, `test-util`, `res`, `library`, `native` |
| `testUtil` | `test-util`, `util` |
| `androidTestUtil` | `android-test-util`, `test-util` |
| `androidRes` | `res`, `widget`, `compose-widget` |
| `widget` | `ui-library`, `widget`, `compose-widget`, `util`, `android-util`, `res` |
| `composeWidget` | `ui-library`, `compose-widget`, `widget`, `util`, `android-util`, `res` |
| `viewBinding` | `api`, `widget`, `compose-widget`, `res`, `library`, `android-util`, `ui-library` |
| `androidApp` | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library` |
| `androidBinary` | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library` |
| `androidNative` | *(no project-dep check)* |
| `androidApp` | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library`, `native` |
| `androidBinary` | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library`, `native` |
| `androidNative` | *(leaf — no first-party project deps)* |

Dagger2-friendly `api`/`impl` + composition roots: **F-011** (landed).
Jetpack Compose: per-target `compose` flag + `composeWidget` — **F-013**
Expand Down
1 change: 1 addition & 0 deletions TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ worker (ops); `v2`→`master` (explicit git promote only). **F-094** stays `bloc
| F-112 | todo | Progressive example: navigation ports | GH **#46** teach — `examples/android/12-navigation-ports` per F-102 §4.4; ladder + README; minimal graph; ports vs adapter vs `navigationRes`. Prefer before or with F-111. |
| F-103 | todo | Hybrid targets example (flat dir / api+impl co-location) | GH **#44** — teaching example: simplified flat layout (api/impl/stub mental model, easy multi-module nav). May use includer layout + progressive example; align with F-084/F-088 fleet layout. No restore of `androidLibrary`. |
| F-104 | todo | Hybrid configuration / stub targets for IDE sync | GH **#43** — stub targets + deps API so IDE sync can swap `impl`→`stub` (compileOnly/runtimeOnly pattern) via **one project-global flag**, not per-module hacks. Design+spike; depend on F-101/`target` APIs. Keep matrix truth. |
| F-113 | done | Android first-party library examples complete | Close ladder gaps: real `androidTestUtil` usage in 09; `androidNative` step **13** + matrix edges (`androidUtil`/`app`/`binary`→`native`); `test*`/`androidTest*` deps = `FormaDependency` so project targets work; docs/skills/matrix aligned. |

## P11 — Kotlin Multiplatform (Stepan 2026-07-25)

Expand Down
2 changes: 1 addition & 1 deletion docs/CALL-SITE-SURFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ All return **`Unit`**.
| `widget` | `widget` | deps, test runners | View UI components |
| `composeWidget` | `compose-widget` | deps, test runners | **Type owns** Compose on |
| `uiLibrary` | `ui-library` | deps, **`compose`**, `buildConfiguration` | Shared UI bases |
| `androidNative` | `native` | `buildSystem`, `abi`, … | NDK |
| `androidNative` | `native` | `buildSystem`, `abi`, … | NDK leaf; consumers `androidUtil`/`app`/`binary` |
| Path B derived (e.g. `navigationRes`) | often shared suffix | same as base + type-owned plugins | Consumer-defined; see TARGET-PLUGINS |

Internal helper `resourcesTarget(type, …)` backs `androidRes` and derived res DSLs —
Expand Down
23 changes: 14 additions & 9 deletions docs/DEPENDENCY-MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,16 @@ these suffixes (or are unrestricted if `EmptyValidator`).
| `library` (JVM) | `library.kt` | `validator(util, test-util)` | `util`, `test-util` |
| `uiLibrary` | `uiLibrary.kt` | `validator(widget, compose-widget, util, android-util, res)` | `widget`, `compose-widget`, `util`, `android-util`, `res` |
| `util` | `util.kt` | `validator(util, library)` | `util`, `library` |
| `androidUtil` | `androidUtil.kt` | registry + **kmp-library, kmp-util** | `android-util`, `test-util`, `res`, `library`, `kmp-library`, `kmp-util` |
| `androidUtil` | `androidUtil.kt` | registry + **kmp-library, kmp-util** + **native** | `android-util`, `test-util`, `res`, `library`, `native`, `kmp-library`, `kmp-util` |
| `testUtil` | `testUtil.kt` | `validator(test-util, util)` | `test-util`, `util` |
| `androidTestUtil` | `androidTestUtil.kt` | `validator(android-test-util, test-util)` | `android-test-util`, `test-util` |
| `androidRes` | `androidRes.kt` | `validator(res, widget, compose-widget)` | `res`, `widget`, `compose-widget` |
| `widget` | `widget.kt` | `validator(ui-library, widget, compose-widget, util, android-util, res)` | `ui-library`, `widget`, `compose-widget`, `util`, `android-util`, `res` |
| `composeWidget` | `composeWidget.kt` | `validator(ui-library, compose-widget, widget, util, android-util, res)` | `ui-library`, `compose-widget`, `widget`, `util`, `android-util`, `res` |
| `viewBinding` | `viewBinding.kt` | `validator(api, widget, compose-widget, res, library, android-util, ui-library)` | `api`, `widget`, `compose-widget`, `res`, `library`, `android-util`, `ui-library` |
| `androidApp` | `androidApp.kt` | registry + **kmp-api, kmp-library, kmp-util** | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library`, `kmp-api`, `kmp-library`, `kmp-util` |
| `androidBinary` | `androidBinary.kt` | registry + **kmp-api, kmp-library, kmp-util** | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library`, `kmp-api`, `kmp-library`, `kmp-util` |
| `androidNative` | `androidNative.kt` | *(no `applyDependencies`)* | *no project-dep validation* |
| `androidApp` | `androidApp.kt` | registry + **kmp-*** + **native** | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library`, `native`, `kmp-api`, `kmp-library`, `kmp-util` |
| `androidBinary` | `androidBinary.kt` | registry + **kmp-*** + **native** | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library`, `native`, `kmp-api`, `kmp-library`, `kmp-util` |
| `androidNative` | `androidNative.kt` | leaf (empty allow list) | *no first-party project deps* |

### Explicit non-edges (important product rules)

Expand All @@ -153,6 +153,11 @@ These follow from the table (not from separate deny-lists):
- **`androidUtil` may depend on JVM `library`** — Android helpers wrapping pure JVM code (F-061).
- **`androidApp` / `androidBinary` cannot depend on other `binary`** —
single APK composition root (F-011; `app` may not depend on `app`/`binary`).
- **`androidUtil` / composition roots may depend on `native`** — JNI façade
on `androidUtil`; packaging at `androidApp` / `androidBinary` (F-113).
Prefer not teaching `impl` → `native` (no edge).
- **`androidNative` is a leaf** — empty first-party allow list (C/C++ + system
libs only; no `applyDependencies` on the DSL today).
- **Circular-ish UI graph is intentional while experimental:** `uiLibrary`
may depend on `widget` / `compose-widget`, and `widget` / `compose-widget`
may depend on `ui-library` and on each other so View + Compose can coexist
Expand Down Expand Up @@ -200,7 +205,7 @@ Legend:
- **Y** — allowed by non-empty `validator(...)` list
- **\*** — `EmptyValidator` / no project-dep check (anything goes)
- **—** — not allowed by the live validator list
- **n/a** — `androidNative` has no `applyDependencies`
- **leaf** — `androidNative` has empty first-party allow list (no project deps)

Rows = **consumer**. Columns = **dependency type (suffix)**.

Expand All @@ -211,16 +216,16 @@ Rows = **consumer**. Columns = **dependency type (suffix)**.
| **library** (JVM) | — | — | — | — | Y | Y | — | — | — | — | — | — | — | — | — |
| **uiLibrary** | — | — | — | — | Y | — | Y | — | Y | — | Y | Y | — | — | — |
| **util** | — | — | Y | — | Y | — | — | — | — | — | — | — | — | — | — |
| **androidUtil** | — | — | Y | — | — | Y | Y | — | Y | — | — | — | — | — | |
| **androidUtil** | — | — | Y | — | — | Y | Y | — | Y | — | — | — | — | — | Y |
| **testUtil** | — | — | — | — | Y | Y | — | — | — | — | — | — | — | — | — |
| **androidTestUtil** | — | — | — | — | — | Y | — | Y | — | — | — | — | — | — | — |
| **androidRes** | — | — | — | — | — | — | — | — | Y | — | Y | Y | — | — | — |
| **widget** | — | — | — | Y | Y | — | Y | — | Y | — | Y | Y | — | — | — |
| **composeWidget** | — | — | — | Y | Y | — | Y | — | Y | — | Y | Y | — | — | — |
| **viewBinding** | Y | — | Y | Y | — | — | Y | — | Y | — | Y | Y | — | — | — |
| **androidApp** | Y | Y | Y | Y | Y | Y | Y | — | Y | Y | Y | Y | — | — | |
| **androidBinary** | Y | Y | Y | Y | Y | Y | Y | — | Y | Y | Y | Y | Y | — | |
| **androidNative** | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a |
| **androidApp** | Y | Y | Y | Y | Y | Y | Y | — | Y | Y | Y | Y | — | — | Y |
| **androidBinary** | Y | Y | Y | Y | Y | Y | Y | — | Y | Y | Y | Y | Y | — | Y |
| **androidNative** | leaf | leaf | leaf | leaf | leaf | leaf | leaf | leaf | leaf | leaf | leaf | leaf | leaf | leaf | leaf |

Self-deps: a type may depend on itself only if its own suffix is in its
allowed list (e.g. `api`→`api` **Y**, `impl`→`impl` **—**, `widget`→`widget`
Expand Down
14 changes: 14 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

Newest entries first.

## 2026-07-26 — F-113: Android first-party library examples complete

- **Ticket:** F-113 → `done`
- **Branch:** `forma/F-113-android-library-examples` (from `origin/v2`)
- **Why:** Progressive ladder claimed full Android target coverage but (1) `androidTestUtil` was declared without any androidTest consumer, (2) `androidNative` was docs-only and **unconsumable** (no matrix edges), (3) `androidTestDependencies` / `testDependencies` were typed `NamedDependency` so first-party project targets could not be passed.
- **Code:**
- Matrix: `androidUtil` / `androidApp` / `androidBinary` → `native`; native remains a leaf (empty allow list). Kit + registry + unit assertions.
- DSL: `testDependencies` / `androidTestDependencies` widened to `FormaDependency` on `impl`, `androidApp`, `widget`, `composeWidget`, `uiLibrary`, `library`.
- `examples/android/09-test-utils`: instrumented `AdderAndroidTest` uses `AndroidChecks` via `androidTestDependencies = deps(target(":common:android-test-util")) + …`.
- `examples/android/13-android-native`: CMake `androidNative` + JNI `androidUtil` façade + Activity usage.
- **Docs:** DEPENDENCY-MATRIX, PROGRESSIVE-EXAMPLES, CALL-SITE-SURFACE, README, examples/README, agent skill `forma-android-targets` coverage table.
- **Verify:** plugins unit + jacoco; 09 assemble + unit + assembleDebugAndroidTest; 13 assembleDebug (NDK 28.2 + CMake 3.22.1 on host).
- **Next step:** F-111 / F-112 navigation ports (board top `todo`).

## 2026-07-26 — F-102: Navigation abstraction design (GH #46)

- **Ticket:** F-102 → `done` (design bar only). Implement follow-ups **F-111** (sample ports + root adapter), **F-112** (`examples/android/12-navigation-ports`).
Expand Down
4 changes: 2 additions & 2 deletions docs/PROGRESSIVE-EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ narrative tutorials when you want a **minimal buildable project per concept**.
| Multi-feature composition | 07 | 04 | — | forma-project-layout |
| `projectDependencies` catalogs (**house style**) | 08 | — | — | forma-deps-catalog |
| `testUtil` | 09 | 05 | — | platform skills |
| `androidTestUtil` | 09 | — | — | forma-android-targets |
| `androidTestUtil` | 09 (androidTest uses helpers) | — | — | forma-android-targets |
| Type-owned target plugins (`navigationRes` Path B) | 10 | — | — | forma-target-plugins |
| Metro DI (`metroImpl` / `metroApp`, Path A on impl/app) | 11 | — | — | forma-target-plugins |
| Navigation ports (presentation ports + adapter; Jetpack behind root) | **12** (F-112 todo) | — | — | — (design: [NAVIGATION-ABSTRACTION.md](NAVIGATION-ABSTRACTION.md)) |
| `androidNative` + JNI via `androidUtil` | **13** | — | — | forma-android-targets |
| `kmpLibrary` + `kmpProjectConfiguration` | — | — | 01 | forma-kmp-targets |
| JVM/Android → `kmp.*` consumer edge | — | — | 01 (JVM binary) | forma-kmp-targets |
| Fleet check/generate/migrate (`tools.forma.core.fleet`) | docs | docs | — | forma-fleet-tooling |
| `androidNative` | skill/docs | — | — | forma-android-targets |
| Bazel adapter / sample | skill | skill | — | forma-bazel |

## Verify
Expand Down
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ Gold-standard product apps remain [`application/`](../application/) (Android) an
| 06 | `android/06-compose` | `compose` flags, `composeWidget` |
| 07 | `android/07-multi-feature` | two features at composition root |
| 08 | `android/08-deps-catalog` | `projectDependencies` / `library` / `bundle` / `plugin` |
| 09 | `android/09-test-utils` | `testUtil`, `androidTestUtil` (+ `androidNative` docs) |
| 09 | `android/09-test-utils` | `testUtil`, `androidTestUtil` (unit + instrumented usage) |
| 10 | `android/10-target-plugins` | Path B `navigationRes`, type-owned safe-args, `extraPlugins` classpath |
| 11 | `android/11-metro-di` | Path A Metro on `impl`/`app` + `metroImpl`/`metroApp` DSLs, compile-time DI |
| 13 | `android/13-android-native` | `androidNative` (CMake) + JNI `androidUtil` consumer |

### KMP (`tools.forma.kmp`)

Expand Down
40 changes: 33 additions & 7 deletions examples/agent-skills/forma-android-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ Plugin: `tools.forma.android`. Register happens on first DSL use via `AndroidTar
| `viewBinding` | `viewbinding` | **only** `layout*` under res | 03 |
| `library` | `library` | pure JVM library | 04 |
| `util` | `util` | no res/ | 04 |
| `androidUtil` | `android-util` | no res/ | 04 |
| `androidUtil` | `android-util` | no res/ | 04, **13** (JNI façade) |
| `widget` | `widget` | Custom View | 05 |
| `uiLibrary` | `ui-library` | shared UI blocks | 05 |
| `composeWidget` | `compose-widget` | always Compose | 06 |
| `testUtil` | `test-util` | no res/ | 09 |
| `androidTestUtil` | `android-test-util` | Android test helpers | 09 |
| `androidNative` | `native` | no res/; **no project-dep validation yet** | docs only |
| `testUtil` | `test-util` | no res/ | 09 (unit test usage) |
| `androidTestUtil` | `android-test-util` | Android test helpers | 09 (`androidTestDependencies`) |
| `androidNative` | `native` | no res/; **leaf** (no first-party deps) | **13** |

## Minimal skeletons

Expand All @@ -35,19 +35,44 @@ androidBinary(packageName = "…", versionCode = 1, versionName = "0.1.0", depen
androidApp(packageName = "…", dependencies = deps("androidx.appcompat:appcompat:1.6.1") + deps(target(":root-res")))
androidRes(packageName = "…")
api(packageName = "…")
impl(packageName = "…", dependencies = deps(target(":feature:x:api")))
impl(
packageName = "…",
dependencies = deps(target(":feature:x:api")),
// FormaDependency: named GAVs and/or first-party test helpers
testDependencies = transitiveDeps("junit:junit:4.13.2"),
androidTestDependencies = deps(target(":common:android-test-util")) + transitiveDeps(
"androidx.test.ext:junit:1.2.1",
),
)
viewBinding(packageName = "…", dependencies = deps(target(":feature:x:res")))
widget(packageName = "…")
composeWidget(packageName = "…", dependencies = deps(/* compose GAVs */))
// androidLibrary(…) // REMOVED F-063 — use androidUtil / uiLibrary / androidRes / …
androidUtil(packageName = "…")
androidUtil(packageName = "…", dependencies = deps(target(":common:hello:native"))) // JNI façade
util(packageName = "…")
library(packageName = "…")
testUtil(packageName = "…")
androidTestUtil(packageName = "…")
// androidNative(packageName = "…") // NDK; validators incomplete
androidNative(
packageName = "…",
buildSystem = tools.forma.android.config.CMake(path = file("src/main/cpp/CMakeLists.txt")),
abi = setOf(tools.forma.android.config.NdkAbi.ARM8, tools.forma.android.config.NdkAbi.X86_64),
)
```

## First-party library coverage (usage examples)

Every Android **first-party library-like** role has a progressive step with real call-sites + source usage:

| Role | Example | Usage proof |
|------|---------|-------------|
| `library` / `util` / `androidUtil` | 04 | feature code calls helpers |
| `uiLibrary` / `widget` | 05 | Activity hosts `BannerView` |
| `composeWidget` | 06 | Activity `setContent { GreetingCard }` |
| `testUtil` | 09 | `AdderTest` uses `assertPositive` |
| `androidTestUtil` | 09 | `AdderAndroidTest` uses `AndroidChecks` |
| `androidNative` | 13 | Activity shows JNI string via `androidUtil` |

## Forbidden

- `.withPlugin` / free-form plugin id lists — use type-owned plugins (see forma-target-plugins)
Expand All @@ -57,3 +82,4 @@ androidTestUtil(packageName = "…")
- Wrong suffix for DSL
- Relying on transitive feature wiring instead of listing api+impl on roots
- Putting `versionCode`/`versionName` on `androidApp` or `androidProjectConfiguration` — only on `androidBinary`
- `impl` → `native` (no matrix edge) — put JNI on `androidUtil`
21 changes: 14 additions & 7 deletions examples/android/09-test-utils/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
# 09 — test-utils (Android)

**Goal:** Shared unit / instrumentation test helpers.
**Goal:** Shared unit / instrumentation test helpers — real **usage**, not empty modules.

## Features introduced
- `testUtil` (shared unit-test code, no res/)
- `androidTestUtil` (shared Android test code)
- `testDependencies` / `androidTestDependencies` on `impl`
- `testUtil` (shared unit-test code, no res/) — used from `src/test` via main/`testDependencies`
- `androidTestUtil` (shared Android instrumented-test helpers) — used from `src/androidTest` via `androidTestDependencies`
- `testDependencies` / `androidTestDependencies` on `impl` accept **named GAVs and first-party project targets** (FormaDependency)

## androidNative (documented)
`androidNative` exists for NDK modules (suffix `native`). It has **no project-dep validation yet**.
Not required for this step; see agent skill `forma-android-targets`.
## Layout
- `common/test-util` — `assertPositive` used by `AdderTest`
- `common/android-test-util` — `AndroidChecks` used by `AdderAndroidTest`
- `feature/hello/impl` — unit + instrumented tests

## Build
```bash
source ../../../scripts/env-mac.sh
printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
./gradlew :binary:assembleDebug
./gradlew :feature-hello-impl:testDebugUnitTest
# Compile instrumented tests (no device required):
./gradlew :feature-hello-impl:assembleDebugAndroidTest
```

## Next
`androidNative` has its own ladder step: [13-android-native](../13-android-native).
Navigation ports: [12-navigation-ports](../12-navigation-ports) (F-112).
Loading
Loading