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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ systems like Buck and Bazel.
2. [**JVM getting started**](docs/JVM-GETTING-STARTED.md) — pure JVM tutorial, run the sample, greenfield skeleton with `binary`, `api`/`impl`, target cheat sheet (F-032)
3. [Sample app gold standard](docs/SAMPLE-APP.md) — multi-feature layout to copy
4. [Dependency matrix](docs/DEPENDENCY-MATRIX.md) — what may depend on what
5. [External deps catalogs](docs/DEPS-CATALOG.md) · [Target plugins API design](docs/TARGET-PLUGINS.md) (F-070: Bazel-like type-owned plugins) · [Compose](docs/COMPOSE.md) · [Environment](docs/ENV.md)
5. [External deps catalogs](docs/DEPS-CATALOG.md) · [Target plugins](docs/TARGET-PLUGINS.md) (type-owned, Bazel-like; example `examples/android/10-target-plugins`) · [Compose](docs/COMPOSE.md) · [Environment](docs/ENV.md)
6. [Plugin publish path](docs/PLUGIN-PUBLISH.md) — Portal metadata DSL + release notes (F-016)
7. [Configuration performance](docs/CONFIGURATION-PERFORMANCE.md) — measure + hot-path guidance (F-017 / GH #106)
8. [forma-core public API design](docs/forma-core-api.md) — extraction contract for types / restrictions / registry (F-020)
Expand Down
2 changes: 1 addition & 1 deletion TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Close the biggest call-site / multi-way gap: chain `withPlugin`. Design:
| F-070 | done | Design type-owned plugins API | Bazel north star; type owns plugin; auto-apply; reject free-form lists + call-site binding re-selection |
| F-071 | done | **Implement** type→plugin registry + auto-apply; `targetPlugin` / `deriveTargetType` | `TargetPluginSpec` + registry + Path A/B; `applyTargetPlugins` on all Android DSLs; unit tests; plugins build green; legacy shims kept for F-072 |
| F-072 | done | Migrate sample to derived types (e.g. `navigationRes`); hard-deprecate chain | Path B `navigationRes` in build-dependencies; `resourcesTarget` helper; Unit returns on androidRes/binary/uiLibrary; `TargetBuilder`/`PluginWrapper`/`Plugins` `@Deprecated`; zero active `.withPlugin`; application build green |
| F-073 | todo | User docs + progressive example + agent skill; close GH #36 | Call sites attributes-only; registration/type layer documented |
| F-073 | done | User docs + progressive example + agent skill; close GH #36 | `docs/TARGET-PLUGINS.md` quick start; `examples/android/10-target-plugins` Path B; agent skill `forma-target-plugins`; GH #36 closable |

## P8 — Principle alignment (implementation matches goals)

Expand Down
25 changes: 22 additions & 3 deletions docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,34 @@ buildscript {
agpVersion = "8.13.2", // keep aligned with plugin compile AGP
// compose = false, // project default for per-target compose flags
// composeCompilerVersion = "2.0.21", // match your Kotlin (2.0.21 → 2.0.21)
// Classpath only — does NOT apply plugins to modules. See TARGET-PLUGINS.md.
extraPlugins = listOf(
// optional: navigation safe-args, KSP, crashlytics, …
// e.g. libs.plugins.navigationSafeArgs (jar on buildscript classpath)
),
)
}
```

This stores shared Android settings, puts AGP on the buildscript classpath, and
registers a root `clean` task. Child targets read these values automatically.
This stores shared Android settings, puts AGP (and optional **plugin jars**) on
the buildscript classpath, and registers a root `clean` task. Child targets read
these values automatically.

### External Gradle plugins (safe-args, Firebase, …)

**Do not** call `.withPlugin` on targets (deprecated). Plugin identity belongs on
the **target type**:

| Path | Use when |
|------|----------|
| **B** `deriveTargetType` + thin DSL (e.g. `navigationRes`) | Only some modules need the plugin (**preferred**) |
| **A** `registerTargetPlugin(predefinedType, …)` | Every module of that kind should get the plugin |

Call sites stay attributes-only. Full contract: [TARGET-PLUGINS.md](TARGET-PLUGINS.md).
Hands-on: [examples/android/10-target-plugins](../examples/android/10-target-plugins).
Sample: `application/core/navigation/res` + `build-dependencies/.../NavigationRes.kt`.

`extraPlugins` / catalog `plugin(...)` = **classpath**. Type-owned registry =
**apply** on the right targets.

Useful `gradle.properties` (sample-aligned):

Expand Down
26 changes: 26 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

Newest entries first.

## 2026-07-20 — F-073: target plugins user docs + progressive example + agent skill

- **Ticket:** F-073 → `done`
- **Branch / PR:** `forma/F-073-target-plugins-docs` → base `v2`
- **Skills/modes:** Grok Build unavailable (503 + max-turns on stale F-072 plan); Hermes implement docs/example slice directly (F-073 is docs/example class)
- **Docs / teaching:**
- `docs/TARGET-PLUGINS.md` — status F-070–F-073 shipped + **Quick start (users)**
- `docs/GETTING-STARTED.md` — external plugins section; `extraPlugins` = classpath only
- `docs/SAMPLE-APP.md` — `navigation/res` is Path B `navigationRes`
- `docs/PROGRESSIVE-EXAMPLES.md` + `examples/README.md` — Android step **10**
- Root `README.md` — target plugins link points at shipped how-to + example
- **Progressive example:** `examples/android/10-target-plugins/`
- Local `forma-defs/` included build: `targetPlugin` + `deriveTargetType` + `navigationRes` DSL
- Call site `feature/hello/res` uses `navigationRes(...)` (no plugin ids)
- `root-res` stays plain `androidRes` (contrast)
- Classpath: catalog `plugin(...)` + `extraPlugins` for forma-defs + safe-args
- **Agent skill:** `examples/agent-skills/forma-target-plugins.md` + README/overview/android-targets pointers
- **Verify (real host, `source scripts/env-mac.sh`):**
- `examples/android/10-target-plugins ./gradlew :binary:assembleDebug` → **BUILD SUCCESSFUL**
- `:feature-hello-res` runs safe-args / navigation resource processing (type-owned plugin applied)
- `plugins/ ./gradlew :deps:test :android:compileKotlin` → run in same session
- **GH #36:** close with comment pointing at docs + example
- **Blockers:** none
- **Next:** F-081 call-site surface audit (P8)


## 2026-07-19 — F-072: navigationRes Path B + deprecate withPlugin chain

- **Ticket:** F-072 → `done`
Expand Down
1 change: 1 addition & 0 deletions docs/PROGRESSIVE-EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ narrative tutorials when you want a **minimal buildable project per concept**.
| `projectDependencies` catalogs | 08 | — | forma-deps-catalog |
| `testUtil` | 09 | 05 | platform skills |
| `androidTestUtil` | 09 | — | forma-android-targets |
| Type-owned target plugins (`navigationRes` Path B) | 10 | — | forma-target-plugins |
| `androidNative` | skill/docs | — | forma-android-targets |
| Bazel adapter / sample | skill | skill | forma-bazel |

Expand Down
10 changes: 9 additions & 1 deletion docs/SAMPLE-APP.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ application/
├── core/
│ ├── di/android-util androidUtil (Dagger scopes / base component)
│ ├── mvvm/ui-library uiLibrary (ViewModel helpers, adapters)
│ ├── navigation/res androidRes (feature nav graphs + safe-args)
│ ├── navigation/res navigationRes (Path B derived type — nav graphs + type-owned safe-args)
│ ├── network/library library (JVM) (Retrofit / Config / NetworkState)
│ └── theme/{android-util,res}
├── common/
Expand Down Expand Up @@ -122,3 +122,11 @@ build JDK **21** (app language level unchanged). See [ENV.md](ENV.md),

Not in scope for F-014: full navigation redesign (GH #46), publish path (F-016).
Configuration-time performance: [CONFIGURATION-PERFORMANCE.md](CONFIGURATION-PERFORMANCE.md) (F-017).

## Type-owned plugins (navigation)

`core/navigation/res` uses Path B **`navigationRes(...)`** (defined in
`build-dependencies/.../NavigationRes.kt`), not plain `androidRes` + `.withPlugin`.
Safe-args is owned by the derived type and auto-applies. See
[TARGET-PLUGINS.md](TARGET-PLUGINS.md) and progressive example
`examples/android/10-target-plugins/`.
17 changes: 14 additions & 3 deletions docs/TARGET-PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,23 @@ target kind + attributes. Plugin identity is part of the **rule / target type**,
not restated on every module. Extending a type with a plugin **auto-applies** that
plugin on every call site of that type.

**Status:** F-070 design accepted · **F-071** registry + auto-apply · **F-072** sample `navigationRes` Path B + chain `@Deprecated`.
**Remaining:** F-073 docs / progressive example / GH #36.
**Status:** F-070–F-073 **shipped** on `v2`. Registry + auto-apply · sample `navigationRes` Path B · chain `@Deprecated` · user docs + progressive example `examples/android/10-target-plugins` + agent skill `forma-target-plugins` · GH #36 closable.

**Related:** [`DEPS-CATALOG.md`](DEPS-CATALOG.md), [`ARCHITECTURE.md`](ARCHITECTURE.md),
[`forma-core-api.md`](forma-core-api.md), `TargetRegistry` / `TargetRegistration`.


## Quick start (users)

1. Put the Gradle plugin jar on the **classpath** (`extraPlugins` / catalog `plugin(...)`).
2. Bind the plugin to a **type** once:
- **Path B (typical):** `targetPlugin` + `deriveTargetType` + thin DSL (see sample `NavigationRes.kt` / example `forma-defs/`).
- **Path A:** `registerTargetPlugin(AndroidTargetTypes.res, …)` only if *every* module of that kind should get it.
3. Call sites set **attributes only** — never plugin ids, never `.withPlugin`.

Hands-on: [`examples/android/10-target-plugins`](../examples/android/10-target-plugins).
Agent skill: [`forma-target-plugins`](../examples/agent-skills/forma-target-plugins.md).

---

## 1. Problem
Expand Down Expand Up @@ -321,7 +332,7 @@ firebaseBinary(
| **F-070** | This design | Merged on `v2` |
| **F-071** | `targetPlugin`, type→plugin registry, auto-apply in pre-defined DSLs (Path A lookup), `deriveTargetType` core hooks | unit tests: type with plugin applies without call-site API; plugins build green |
| **F-072** | Path B DSL helpers; migrate sample navigation (+ optional firebase binary type); deprecate `TargetBuilder` / `PluginWrapper` | application green; **zero** `.withPlugin` call sites; call sites Bazel-flat |
| **F-073** | Docs + progressive example + agent skill; close GH #36 | examples match §3 mapping |
| **F-073** | Docs + progressive example + agent skill; close GH #36 | **done** — `docs/TARGET-PLUGINS.md` how-to, `examples/android/10-target-plugins`, `forma-target-plugins` skill |

---

Expand Down
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Gold-standard product apps remain [`application/`](../application/) (Android) an
| Track | Path | Steps |
|-------|------|-------|
| JVM | [`jvm/`](jvm/) | 01 hello-binary → 05 test-util |
| Android | [`android/`](android/) | 01 hello-apk → 09 test-utils |
| Android | [`android/`](android/) | 01 hello-apk → 10 target-plugins |
| Agent skills | [`agent-skills/`](agent-skills/) | SKILL docs for coding agents |
| User index | [`docs/PROGRESSIVE-EXAMPLES.md`](../docs/PROGRESSIVE-EXAMPLES.md) | narrative + feature coverage matrix |

Expand All @@ -36,6 +36,7 @@ Gold-standard product apps remain [`application/`](../application/) (Android) an
| 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) |
| 10 | `android/10-target-plugins` | Path B `navigationRes`, type-owned safe-args, `extraPlugins` classpath |

Bazel is covered by agent skill + existing [`bazel-adapter/`](../bazel-adapter/) and [`bazel-sample/`](../bazel-sample/) (not duplicated here).

Expand Down
1 change: 1 addition & 0 deletions examples/agent-skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ matrix rules, and progressive example pointers.
| [forma-jvm-targets](forma-jvm-targets.md) | Pure JVM targets |
| [forma-dependency-matrix](forma-dependency-matrix.md) | Dependency / visibility errors |
| [forma-deps-catalog](forma-deps-catalog.md) | External deps / catalogs |
| [forma-target-plugins](forma-target-plugins.md) | External plugins on types (Path A/B); never `.withPlugin` |
| [forma-compose](forma-compose.md) | Jetpack Compose |
| [forma-includer-settings](forma-includer-settings.md) | settings.gradle / includer / composite |
| [forma-project-layout](forma-project-layout.md) | Feature folders, packages, composition roots |
Expand Down
2 changes: 2 additions & 0 deletions examples/agent-skills/forma-android-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Plugin: `tools.forma.android`. Register happens on first DSL use via `AndroidTar
| `api` | `api` | no `res/` | 02+ |
| `impl` | `impl` | Android feature impl | 02+ |
| `androidRes` | `res` | **only** `res/` under src/main | 01, 03 |
| `navigationRes` (Path B sample/example) | `res` (typical) | same as res + type-owned safe-args | 10 |
| `viewBinding` | `viewbinding` | **only** `layout*` under res | 03 |
| `library` | `library` | pure JVM library | 04 |
| `util` | `util` | no res/ | 04 |
Expand Down Expand Up @@ -47,6 +48,7 @@ androidTestUtil(packageName = "…")

## Forbidden

- `.withPlugin` / free-form plugin id lists — use type-owned plugins (see forma-target-plugins)
- `impl` → `impl`
- `api` shipping `res/`
- removed `androidLibrary` (use role-specific targets; F-063)
Expand Down
2 changes: 2 additions & 0 deletions examples/agent-skills/forma-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ shared configuration, and a **dependency restriction graph**.
- `docs/GETTING-STARTED.md` / `docs/JVM-GETTING-STARTED.md`
- `docs/PROGRESSIVE-EXAMPLES.md`
- `docs/COMPOSE.md`, `docs/DEPS-CATALOG.md`

- **External plugins:** type-owned (Path A/B) — skill `forma-target-plugins`, docs `TARGET-PLUGINS.md`, example `android/10-target-plugins`.
91 changes: 91 additions & 0 deletions examples/agent-skills/forma-target-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
name: forma-target-plugins
description: Type-owned external Gradle plugins (Path A/B). Call sites attributes-only; never .withPlugin.
---

# Target plugins (agent skill)

External Gradle plugins (safe-args, GMS, Crashlytics, …) are **owned by the target type**,
not selected per module. Design: `docs/TARGET-PLUGINS.md`. Progressive step:
`examples/android/10-target-plugins/`. Sample: `build-dependencies/.../NavigationRes.kt`.

## When to load
- Adding Navigation safe-args, Firebase, or any third-party Gradle plugin to a Forma target
- Reviewing PRs that introduce `.withPlugin`, `plugins = plugins(…)`, or free-form plugin ids on DSL calls
- Scaffolding a derived type (Path B) or extending a pre-defined type (Path A)

## Model

| Layer | Owns |
|-------|------|
| Type / rule (`registerTargetPlugin` or `deriveTargetType`) | Plugin **identity** — auto-applies every call site |
| Call site (`build.gradle.kts`) | **Attributes only** (packageName, deps, rule attrs) |

Apply order inside DSL: validate → features → **type-owned plugins** → dependencies.

## Path A — extend pre-defined type (global for that kind)

```kotlin
registerTargetPlugin(
AndroidTargetTypes.res,
targetPlugin(id = "androidx.navigation.safeargs.kotlin"),
)
// Every androidRes(...) now applies safe-args — only if ALL res modules need it
androidRes(packageName = "…", dependencies = deps(…))
```

## Path B — derived type (preferred when selective)

```kotlin
private val navigationSafeArgs = targetPlugin(id = "androidx.navigation.safeargs.kotlin")
val NavigationResType = deriveTargetType(
id = "sample.navigation-res",
base = AndroidTargetTypes.res,
nameSuffix = "res", // keep res suffix for matrix/name matcher unless isolating
plugins = listOf(navigationSafeArgs),
)
fun Project.navigationRes(packageName: String, dependencies: FormaDependency = emptyDependency(), …) {
resourcesTarget(type = NavigationResType, packageName = packageName, dependencies = dependencies, …)
}

// Call site
navigationRes(packageName = "…", dependencies = deps(…))
```

## Classpath vs apply

- `extraPlugins` on `androidProjectConfiguration` and catalog `plugin(...)` put jars on the
**buildscript classpath**. They do **not** replace type-owned apply.
- Applying a plugin to a module = type binding + `applyTargetPlugins(type)` inside the DSL.

## APIs (Android)

```text
tools.forma.android.target.targetPlugin
tools.forma.android.target.registerTargetPlugin
tools.forma.android.target.deriveTargetType
// shared wiring for res-like types:
resourcesTarget(type = …, packageName = …, …)
```

## Forbidden

- `.withPlugin` / `.withPlugins` / `TargetBuilder` chains (deprecated F-072)
- Free-form `plugins = plugins(plugin("id"))` on targets
- Call-site `pluginConfig(binding)` that re-selects plugins
- New forever core DSL per vendor plugin when Path B derived type is enough
- Teaching removed `androidLibrary`

## Separate concerns (do not collapse)

- **FeatureDefinition** — AGP / Kotlin / Compose platform features
- **Catalog Mode-2** — KSP when companion lib is consumed
- **Type-owned plugins** — safe-args / GMS / Crashlytics style external plugins

## Checklist

1. Plugin id appears only in registration (Path A or B), never at call sites
2. Classpath entry exists (`extraPlugins` / `plugin(...)`) for the Gradle plugin jar
3. Derived type clones base allow-list/content rules (`deriveTargetType` + AndroidTargetRegistry)
4. Suffix choice documented (share `res` vs new suffix)
5. Progressive example or sample updated if teaching a new pattern
6 changes: 6 additions & 0 deletions examples/android/10-target-plugins/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

.gradle/
build/
local.properties
forma-defs/.gradle/
forma-defs/build/
59 changes: 59 additions & 0 deletions examples/android/10-target-plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 10 — target-plugins (Android)

**Goal:** Type-owned external Gradle plugins (F-070–F-073). Call sites stay Bazel-flat.

## Features introduced
- **Path B** derived type: `navigationRes` via `targetPlugin` + `deriveTargetType` (see `forma-defs/`)
- Auto-apply of `androidx.navigation.safeargs.kotlin` on every `navigationRes(...)` call site
- `extraPlugins` / catalog `plugin(...)` as **classpath only** (not per-module apply)
- Contrast with plain `androidRes` (no external plugin) on `root-res`

## Mental model

| Layer | Owns |
|-------|------|
| Type registration (`forma-defs` / once) | Plugin identity |
| Call site (`feature/hello/res/build.gradle.kts`) | Attributes only (`packageName`, `dependencies`) |

```kotlin
// Registration once (forma-defs) — not repeated per module
val navigationSafeArgs = targetPlugin(id = "androidx.navigation.safeargs.kotlin")
val ExampleNavigationResType = deriveTargetType(
id = "example.navigation-res",
base = AndroidTargetTypes.res,
nameSuffix = "res",
plugins = listOf(navigationSafeArgs),
)
fun Project.navigationRes(...) = resourcesTarget(type = ExampleNavigationResType, ...)

// Call site — no plugin ids
navigationRes(
packageName = "…",
dependencies = deps("androidx.navigation:navigation-fragment-ktx:2.7.7", …),
)
```

**Path A** (not used here): `registerTargetPlugin(AndroidTargetTypes.res, safeArgs)` would attach
safe-args to *every* `androidRes(...)`. Prefer Path B when only some res modules need the plugin.

## Forbidden
- `.withPlugin(...)` / `.withPlugins(...)` chains (deprecated)
- Free-form `plugins = plugins(plugin("id"))` on targets
- Re-selecting plugin bindings at each call site

## Build
```bash
source ../../../scripts/env-mac.sh
printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
./gradlew :binary:assembleDebug
# Prove type-owned safe-args applied:
./gradlew :feature:hello:res:tasks --all | grep -i safeargs
```

## Docs
- Design + API: [`docs/TARGET-PLUGINS.md`](../../../docs/TARGET-PLUGINS.md)
- Agent skill: [`examples/agent-skills/forma-target-plugins.md`](../../agent-skills/forma-target-plugins.md)
- Gold-standard sample: `application/core/navigation/res` + `build-dependencies/.../NavigationRes.kt`

## Next
P8 principle-alignment tickets (F-081+). Ladder complete for P7 plugins.
15 changes: 15 additions & 0 deletions examples/android/10-target-plugins/binary/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
androidBinary(
packageName = "tools.forma.examples.android.targetplugins",
versionCode = 1,
versionName = "0.1.0",
dependencies = deps(
// Bring navigation attrs into the APK merge (startDestination, etc.)
"androidx.navigation:navigation-runtime:2.7.7",
"androidx.navigation:navigation-common:2.7.7",
"androidx.navigation:navigation-fragment:2.7.7",
) + deps(
target(":root-app"),
target(":feature:hello:api"),
target(":feature:hello:impl")
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<!-- Empty manifest required for AGP binary target -->
</manifest>
Loading
Loading