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](docs/TARGET-PLUGINS.md) (type-owned, Bazel-like; example `examples/android/10-target-plugins`) · [Call-site surface](docs/CALL-SITE-SURFACE.md) (Unit DSLs + flag inventory) · [Fleet tooling](docs/FLEET-TOOLING.md) (check/generate/migrate, F-084) · [Principle audit](docs/PRINCIPLE-AUDIT.md) (F-085) · [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`) · [Call-site surface](docs/CALL-SITE-SURFACE.md) (Unit DSLs + flag inventory) · [Fleet tooling](docs/FLEET-TOOLING.md) (check/generate Gradle tasks + core APIs, F-084/F-088) · [Principle audit](docs/PRINCIPLE-AUDIT.md) (F-085) · [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 @@ -119,7 +119,7 @@ Promoted from daily next-actions / historical GH. Workers pick top `todo` in ord
| ID | Status | Title | Notes |
|----|--------|-------|-------|
| F-087 | done | AndroidX / SDK ceiling after AGP 9 | compile/target **37**; core **1.19.0**, activity **1.13.0**, lifecycle **2.11.0**, compose **1.11.4**, room **2.8.4**, material **1.14.0**; JVM **11**; navigationevent on activity graph; paging **2.1.2** kept. Host+CI platform 37. |
| F-088 | todo | Fleet tooling phase 2 (F-084 follow-ups) | From `docs/FLEET-TOOLING.md`: Gradle `formaLayoutCheck` / `formaLayoutGenerate` tasks (thin shells over core); optional config-time packageName dir hook; bulk generate driver for includer graph. Defer AST migrate / depgen resurrection unless unblocked in-slice. Close GH **#54** if generate path is user-complete. |
| F-088 | done | Fleet tooling phase 2 (F-084 follow-ups) | Gradle `formaLayoutCheck`/`formaLayoutGenerate` + root `*All` via `tools.forma.deps.fleet.registerFormaLayout` on all packageName DSLs; opt-in `checkPackageLayoutAtConfiguration` (default false); res/viewBinding/binary skip source-dir requirement; docs/skill updated. GH **#54** generate path user-complete (Hermes may close). AST migrate / depgen still deferred. |
| F-089 | todo | Navigation task cache broken | GH **#110** — fix navigation/safe-args related task cache so config/build cache is correct; reproduce from issue screenshots; add regression note or test if feasible; plugins+app green. |
| F-090 | todo | Exclude modules from dependency validation | GH **#97** — allow-list / opt-out for forked-in libraries (ExoPlayer-class cases) without weakening default matrix. One global configuration path (not per-call-site escape hatch spam). Docs + sample or unit coverage. |
| F-091 | todo | BuildFeatures under Forma | GH **#88** — map AGP `BuildFeatures` into Forma (default off where safe); single configuration path aligned with type=rule + project-global defaults; document in CALL-SITE / PROJECT-CONFIGURATION. Do not reintroduce fat call-site plugin shopping. |
Expand Down
117 changes: 90 additions & 27 deletions docs/FLEET-TOOLING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Fleet tooling — check / generate / migrate (F-084)
# Fleet tooling — check / generate / migrate (F-084 + F-088)

**Status:** v1 toolkit shipped in `tools.forma:core` (`tools.forma.core.fleet`).
**Status:** v1 pure APIs in `tools.forma:core` (`tools.forma.core.fleet`) **and** phase-2 Gradle shells (F-088).
Related: principle **3** in [`VISION.md`](VISION.md), GH **#54**, includer, bazel-adapter generate/check.

Forma keeps structure **explicit**. At thousands of modules that only works if
Expand All @@ -27,11 +27,11 @@ Forma keeps structure **explicit**. At thousands of modules that only works if

## One global way: three modes

| Mode | Job | v1 entry |
|------|-----|----------|
| **check** | Fail when layout does not match declared `packageName` (CI-friendly pure API) | `LayoutChecker` |
| **generate** | Create missing `src/main/{java\|kotlin}/…` trees from `packageName` | `LayoutGenerator` |
| **migrate** | Plan module path renames + suggested reference rewrites (not AST rewrite) | `MigratePlanner` |
| Mode | Job | Entry |
|------|-----|-------|
| **check** | Fail when layout does not match declared `packageName` | Core `LayoutChecker` · Gradle `formaLayoutCheck` / `formaLayoutCheckAll` |
| **generate** | Create missing `src/main/{java\|kotlin}/…` trees from `packageName` | Core `LayoutGenerator` · Gradle `formaLayoutGenerate` / `formaLayoutGenerateAll` |
| **migrate** | Plan module path renames + suggested reference rewrites (not AST rewrite) | Core `MigratePlanner` (plan-only; AST rewrite deferred) |

Do **not** invent a second parallel layout convention per team. Extend these
helpers (or thin Gradle tasks that call them), not ad-hoc scripts with different
Expand All @@ -43,9 +43,10 @@ path rules.
|------|------------|--------|
| **includer** (`includer/`) | **Discover** modules: any dir with `build.gradle(.kts)` is included; nested `settings` skipped | Naming: `feature/home/impl` → Gradle `:feature-home-impl` |
| **forma-core validators** | **Runtime check** of content rules + restriction graph | Configuration-time; not generate |
| **`tools.forma.core.fleet`** (this doc) | **Offline** check / generate / migrate-plan | Pure Kotlin, unit-tested, no Gradle APIs |
| **`tools.forma.core.fleet`** | **Offline** check / generate / migrate-plan | Pure Kotlin, unit-tested, no Gradle APIs |
| **`tools.forma.deps.fleet`** (F-088) | **Gradle shells** + layout metadata on every `packageName` DSL | Thin tasks over core; shared Android + JVM |
| **bazel-adapter** | Bazel **generate** + **check** from `FormaProjectModel` | F-041; JVM-first; see [`BAZEL-ADAPTER.md`](BAZEL-ADAPTER.md) |
| **depgen** (`depgen/`) | Intended transitive external-deps generation | **Stub** plugin + unfinished task; not on sample critical path; not part of F-084 v1 |
| **depgen** (`depgen/`) | Intended transitive external-deps generation | **Stub** plugin + unfinished task; not on sample critical path; **deferred** (not resurrected in F-088) |

## Path conventions (source of truth)

Expand All @@ -54,7 +55,7 @@ path rules.
| Filesystem relative dir | `feature/home/impl` | Repo layout + includer walk |
| Gradle project path | `:feature-home-impl` | Includer (`/` → `-`) |
| Forma target ref | `:feature:home:impl` | `target(":feature:home:impl")` |
| Package source dir | `src/main/kotlin/com/foo/bar` | `packageName` attribute |
| Package source dir | `src/main/kotlin/com.foo.bar` | `packageName` attribute |

Helpers: `ProjectPathForms`, `PackageLayout`.

Expand Down Expand Up @@ -96,50 +97,112 @@ MigratePlanner.planRename(
// → filesystemMoves + referenceRewrites (:gradle, :forma, target(), project())
```

### Using from a one-off script / future task
Logic **stays in core**. Gradle tasks are thin shells only.

1. Depend on `tools.forma:core` (same version as Forma plugins).
2. Call pure APIs above (temp dirs in tests; real module dirs in tools).
3. Wire a Gradle task later if desired — **logic stays in core**, task is a thin shell.
## Gradle tasks (F-088 phase 2)

v1 does **not** ship a Gradle plugin task or CLI binary; the library + tests are
the product surface so agents and future tasks share one implementation.
Every Android and JVM target DSL that takes `packageName` calls
`Project.registerFormaLayout(packageName)` (`tools.forma.deps.fleet`):

## What v1 ships vs follow-ups
1. Stores typed metadata on the project: extension `formaLayout` /
`FormaLayoutExtension` (`packageName` + `SourceLanguage`, default KOTLIN).
2. Registers per-project tasks:
- **`formaLayoutCheck`** — for code targets, package dir must exist under **either**
`src/main/kotlin/…` or `src/main/java/…` (sample Android trees use the java root
for Kotlin sources). Fails if missing under both. **Skipped** (success) for
AGP-identity-only targets: `androidRes` / `resourcesTarget`, `viewBinding`,
`androidBinary` (`requirePackageSourceDir = false`).
- **`formaLayoutGenerate`** — `LayoutGenerator.apply(...)` for code targets; logs
created dirs. Prefers an existing conventional root (`java` vs `kotlin`);
otherwise default language KOTLIN. Optional placeholder via project property
**`forma.layout.createPlaceholder=true`** (default **false**). **Skipped** for
AGP-identity-only targets (same set as check skip) so bulk generate does not
drop empty kotlin trees into res/binary modules.
3. Idempotent if the helper is invoked twice on the same project.

Root aggregates (registered from `androidProjectConfiguration` and/or first layout registration):

| Task | Behavior |
|------|----------|
| **`formaLayoutCheckAll`** | `dependsOn` every subproject `formaLayoutCheck` that registered metadata |
| **`formaLayoutGenerateAll`** | `dependsOn` every subproject `formaLayoutGenerate` that registered metadata |

### Optional configuration-time check

On `androidProjectConfiguration` / `AndroidProjectSettings`:

```kotlin
androidProjectConfiguration(
project = rootProject,
// ...
checkPackageLayoutAtConfiguration = false, // default — leave off for sample green
)
```

When **`true`**, after a target registers layout metadata, if the package source
dir is missing → `logger.error` + `GradleException` at **configuration** time.

- **Opt-in** CI/dev strictness only.
- Happy path for scaffolding remains **`formaLayoutGenerate`** / **`formaLayoutGenerateAll`**.
- Pure JVM projects without Android settings skip the config-time hook (no second configuration path).

### Verify commands

```bash
source scripts/env-mac.sh
cd plugins && ./gradlew :core:test build

cd application
./gradlew help
./gradlew :feature-home-api:formaLayoutCheck
./gradlew formaLayoutCheckAll
# scaffold missing trees (explicit; does not run on normal build):
./gradlew :some-module:formaLayoutGenerate
./gradlew formaLayoutGenerateAll
# optional placeholder files:
./gradlew formaLayoutGenerateAll -Pforma.layout.createPlaceholder=true
```

Do **not** enable `checkPackageLayoutAtConfiguration` on the sample by default.

## What ships vs deferred

**Shipped (F-084 v1)**

- Design (this doc) + agent skill
- Pure `check` / `generate` / `migrate-plan` + path forms in `plugins/core`
- Unit tests including GH #54 package example
- Cross-links from README / VISION / GETTING-STARTED

**Follow-ups (not blocking F-084 done)** — board: **F-088**
**Shipped (F-088 phase 2)**

- Gradle `formaLayoutCheck` / `formaLayoutGenerate` on every `packageName` target
- Root `formaLayoutCheckAll` / `formaLayoutGenerateAll`
- Opt-in `checkPackageLayoutAtConfiguration` (default false)
- Docs + agent skill task usage
- GH **#54** generate path is user-complete via Gradle tasks (close when merged)

**Still deferred**

- Gradle `formaLayoutCheck` / `formaLayoutGenerate` tasks on Android/JVM platforms
- Optional configuration-time hook: fail if `packageName` dir missing (opt-in)
- AST-safe build-script rewrite for migrate (IntelliJ/PSI or Kotlin compiler)
- packageName rename paired with directory move
- depgen resurrection or retire (separate decision)
- Deeper bazel-adapter integration with the same `FormaProjectModel` export
- Bulk “generate missing trees for whole includer graph” driver

## Non-goals (v1)
## Non-goals

- Full monorepo rewrite engine
- Teaching a second layout style beside includer + `packageName`
- Android `res/` scaffolding (content rules already constrain res targets)
- Finishing depgen transitive generation
- Mass-creating missing dirs during normal `build` (tasks are explicit)

## Agent skill

See [`examples/agent-skills/forma-fleet-tooling.md`](../examples/agent-skills/forma-fleet-tooling.md).

## Verify
## Verify (plugins unit + shells compile)

```bash
source scripts/env-mac.sh
cd plugins && ./gradlew :core:test
# or full
./gradlew build
cd plugins && ./gradlew :core:test build
```
6 changes: 4 additions & 2 deletions docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ visibility validation** for you.
**Root principles** (see [`VISION.md`](VISION.md)): (1) Bazel-like rules — configure
once on the type, minimal call-site attrs; (2) one global way project-wide; (3)
explicit structure, with tooling for large-scale change
([`FLEET-TOOLING.md`](FLEET-TOOLING.md) — check / generate / migrate).
([`FLEET-TOOLING.md`](FLEET-TOOLING.md) — `formaLayoutCheck` / `formaLayoutGenerate` + core APIs).

This tutorial gets you from zero to a working multi-target Android app. For the
full multi-feature reference layout, see [SAMPLE-APP.md](SAMPLE-APP.md).
Expand Down Expand Up @@ -297,6 +297,8 @@ impl(
```

Put Kotlin under `src/main/java/<package-as-dirs>/…` matching `packageName`.
Scaffold missing package trees with `./gradlew :module:formaLayoutGenerate` or root
`formaLayoutGenerateAll` (see [FLEET-TOOLING.md](FLEET-TOOLING.md)).

### 4.4 Build

Expand Down Expand Up @@ -445,7 +447,7 @@ Details: [COMPOSE.md](COMPOSE.md). Sample:
| SDK / `compileSdk` errors | Install platform **37** (`platforms;android-37.0` + `android-37` symlink); set `local.properties` `sdk.dir` |
| Project not included | Missing `build.gradle.kts`, or nested `settings.gradle.kts` blocked Includer |
| Illegal project dependency | Matrix violation — see [DEPENDENCY-MATRIX.md](DEPENDENCY-MATRIX.md) |
| Empty / wrong package | `packageName` ≠ directory under `src/main/java` |
| Empty / wrong package | `packageName` ≠ directory under `src/main/java` — run `formaLayoutCheck` / `formaLayoutGenerate` |
| Compose compiler mismatch | Align `composeCompilerVersion` with Kotlin (sample: 2.3.21 ↔ 2.3.21) |
| AGP resolution conflicts | Align consumer `agpVersion` with plugin AGP line (**9.3.0** today) |

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

Newest entries first.

## 2026-07-21 — F-088: Fleet tooling phase 2

- **Ticket:** F-088 → `done`
- **Branch:** `forma/F-088-fleet-phase2` (from origin/v2 @ 4fe663a)
- **Actions:**
- Shared Gradle helper `tools.forma.deps.fleet` (`FormaLayoutExtension`, `registerFormaLayout`, root `ensureFormaLayoutRootTasks`)
- Per-project tasks `formaLayoutCheck` / `formaLayoutGenerate` (thin shells over core `LayoutChecker` / `LayoutGenerator`)
- Root aggregates `formaLayoutCheckAll` / `formaLayoutGenerateAll` (`dependsOn` registered subprojects)
- Wired into **all** Android + JVM target DSLs that take `packageName`
- Opt-in `AndroidProjectSettings.checkPackageLayoutAtConfiguration` (default **false**); set via `androidProjectConfiguration(...)`; pure JVM skips when settings unset
- Check accepts either `src/main/java` or `src/main/kotlin` package trees (sample uses java root)
- `requirePackageSourceDir = false` for `resourcesTarget` / `viewBinding` / `androidBinary` (AGP identity only — avoids checkAll fail + bulk-generate pollution on res)
- Docs: `docs/FLEET-TOOLING.md` phase-2 shipped; agent skill task usage; README + GETTING-STARTED cross-links
- **Deferred (documented):** AST migrate rewrite, depgen resurrection
- **GH #54:** generate path is user-complete via Gradle tasks + docs — leave note for Hermes to close (not closed from this CLI)
- **Verify (real host, `source scripts/env-mac.sh`):**
- `plugins/ ./gradlew :core:test build` → **BUILD SUCCESSFUL** (78 tasks)
- `application/ ./gradlew help :feature-home-api:formaLayoutCheck formaLayoutCheckAll --no-configuration-cache` → **BUILD SUCCESSFUL** (82 tasks)
- Sample does **not** set `checkPackageLayoutAtConfiguration`
- Accidental res kotlin trees from an intermediate `generateAll` run were cleaned (`git clean`); generate now skips AGP-identity targets
- **Blockers:** none
- **Next:** F-089 Navigation task cache broken (GH #110)

## 2026-07-21 — F-087: AndroidX / SDK ceiling

- **Ticket:** F-087 → `done`
Expand Down
42 changes: 37 additions & 5 deletions examples/agent-skills/forma-fleet-tooling.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,60 @@
---
name: forma-fleet-tooling
description: Check/generate/migrate package layout and path forms (F-084 fleet toolkit).
description: Check/generate/migrate package layout and path forms (F-084/F-088 fleet toolkit).
---

# Fleet tooling (agent skill)

Canonical design: [`docs/FLEET-TOOLING.md`](../../docs/FLEET-TOOLING.md).
Implementation: `tools.forma.core.fleet` in `plugins/core` (pure Kotlin).
- Pure APIs: `tools.forma.core.fleet` in `plugins/core`
- Gradle shells: `tools.forma.deps.fleet` (`registerFormaLayout` from every `packageName` DSL)

## When to use

- Scaffolding a new target after writing `api` / `impl` / … with `packageName`
- CI or pre-commit layout check for `packageName` ↔ source dir
- Planning a module path rename (includer path + Gradle + `target()` refs)
- Bulk generate for the whole includer graph after adding many modules

## Rules

1. **One way:** use `PackageLayout` / `LayoutGenerator` / `LayoutChecker` / `MigratePlanner` — do not invent alternate path math.
1. **One way:** use `PackageLayout` / `LayoutGenerator` / `LayoutChecker` / `MigratePlanner` — do not invent alternate path math. Gradle tasks are thin shells over the same APIs.
2. `packageName` must match `src/main/kotlin|java/<segments>`.
3. Includer: `feature/home/impl` → `:feature-home-impl`; Forma refs use `:feature:home:impl`.
4. Migrate planner is **plan-only** (string suggestions). Review before bulk replace; not AST-safe.
4. Migrate planner is **plan-only** (string suggestions). Review before bulk replace; not AST-safe (deferred).
5. Still obey matrix / `impl` ↛ `impl` / no `androidLibrary` / type-owned plugins.
6. Do **not** turn on `checkPackageLayoutAtConfiguration` on the sample by default; use explicit generate/check tasks.
7. `androidRes` / `viewBinding` / `androidBinary` register layout metadata but **skip** check/generate (packageName is AGP identity only).

## Snippets
## Gradle tasks (preferred for day-to-day)

Per project (registered when the target DSL runs):

```bash
./gradlew :feature-home-api:formaLayoutCheck
./gradlew :feature-home-api:formaLayoutGenerate
# optional .gitkeep under empty package dir:
./gradlew :feature-home-api:formaLayoutGenerate -Pforma.layout.createPlaceholder=true
```

Whole graph (root aggregates):

```bash
./gradlew formaLayoutCheckAll
./gradlew formaLayoutGenerateAll
```

Opt-in configuration-time fail (Android only, default **false**):

```kotlin
androidProjectConfiguration(
project = rootProject,
// ...
checkPackageLayoutAtConfiguration = true, // CI/strict only
)
```

## Core API snippets (scripts / offline)

```kotlin
import tools.forma.core.fleet.*
Expand Down
2 changes: 2 additions & 0 deletions plugins/android/src/main/java/androidApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import tools.forma.deps.core.FormaDependency
import tools.forma.deps.core.NamedDependency
import tools.forma.deps.core.applyDependencies
import tools.forma.deps.core.applyTargetPlugins
import tools.forma.deps.fleet.registerFormaLayout
import tools.forma.owners.NoOwner
import tools.forma.owners.Owner
import tools.forma.validation.asValidator
Expand Down Expand Up @@ -45,6 +46,7 @@ fun Project.androidApp(

val selfV = AndroidTargetRegistry.selfValidator(AndroidTargetTypes.app).asValidator()
selfV.validate(target)
registerFormaLayout(packageName)
val libraryFeatureConfiguration = AndroidLibraryFeatureConfiguration(
packageName,
buildConfiguration,
Expand Down
Loading
Loading