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) · [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/migrate, F-084) · [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
6 changes: 3 additions & 3 deletions TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,16 @@ Close the biggest call-site / multi-way gap: chain `withPlugin`. Design:
## P8 — Principle alignment (implementation matches goals)

Close remaining gaps where code/docs still allow **multiple ways**, **fat call
sites**, or **missing fleet tooling**. P7 (F-070–F-073) and F-081–F-083 are **done**; **F-019 Phase 1 done**.
**Next coding:** **F-084**
sites**, or **missing fleet tooling**. P7 (F-070–F-073) and F-081–F-084 are **done**; **F-019 Phase 1 done**.
**Next coding:** **F-085**

| ID | Status | Title | Notes |
|----|--------|-------|-------|
| F-080 | done | Codify root principles in product docs | `VISION.md` § Root principles + README + `AGENTS.md` + GETTING-STARTED; board P8 added |
| F-081 | done | Call-site surface audit: `Unit` returns, no builder chains, minimal attrs | Hard-removed `TargetBuilder` / `PluginWrapper` / sample `Plugins` + chain-only `PluginConfiguration`; all Android/JVM DSLs already `Unit`; `docs/CALL-SITE-SURFACE.md` flag inventory (`compose` → project-global default, `viewBinding` on impl + dedicated type) |
| F-082 | done | One global configuration path | Hard-removed `Project.androidProjectConfiguration`; `ScriptHandlerScope` form is the single API; `extraPlugins` documented classpath-only; new `PROJECT-CONFIGURATION.md` + cross-links; builds green |
| F-083 | done | One project-global external-deps convention | **House style = `projectDependencies` → `libs.*`**. Typed `build-dependencies/` catalogs = advanced/sample-scale only (not dual happy path). `DEPS-CATALOG.md` + README/GETTING-STARTED/agent skill/08 README |
| F-084 | todo | Fleet tooling for explicit graphs (check / generate / migrate) | Principle 3 at scale: tighten includer/depgen/bazel-check; design+spike migrate helpers for renames/type changes; related GH **#54**. Reliable large refactors without hand-editing thousands of modules |
| F-084 | done | Fleet tooling for explicit graphs (check / generate / migrate) | **v1 in `tools.forma.core.fleet`:** `LayoutChecker` / `LayoutGenerator` / `MigratePlanner` / path forms; `docs/FLEET-TOOLING.md` + agent skill; GH **#54** package→dir covered. Follow-ups: Gradle tasks, AST migrate, depgen — listed in FLEET-TOOLING.md |
| F-085 | todo | Full-tree principle audit: sample + examples + agent skills | After F-072/F-081: no chain APIs, no dual styles taught, call sites minimal; fix stragglers |

## Backlog (lower priority / historical GitHub)
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Living map of the monorepo build graph, plugin modules, sample app, CI, and
what should become **forma-core**. Source of truth for F-002; update when the
graph changes.

Last audited: **2026-07-19** (F-018 8.x terminal toolchain on `v2`).
Last audited: **2026-07-20** (F-084 fleet toolkit in `tools.forma.core.fleet`; toolchain Gradle 9.6.1).

---

Expand Down
145 changes: 145 additions & 0 deletions docs/FLEET-TOOLING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Fleet tooling — check / generate / migrate (F-084)

**Status:** v1 toolkit shipped in `tools.forma:core` (`tools.forma.core.fleet`).
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
**tooling** can verify and reshape the graph without hand-editing every path.

## Problem

- Renames, type migrations, and new targets touch filesystem layout, Gradle
project names (includer), Forma `target(":…")` refs, and `packageName` source
roots together.
- Runtime validators (`ContentRule`, dependency matrix) catch some mistakes
**after** configuration — too late for bulk refactors and greenfield scaffolding.
- GH #54: from a minimal declaration such as

```kotlin
api(
packageName = "com.stepango.blockme.character.list.api",
owner = Teams.core,
)
```

the package directory `src/main/kotlin/com/stepango/blockme/character/list/api`
should be **generatable**, not hand-created.

## 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` |

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
path rules.

## Tool map

| Tool | Role today | Notes |
|------|------------|--------|
| **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 |
| **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 |

## Path conventions (source of truth)

| Form | Example | Producer |
|------|---------|----------|
| 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 |

Helpers: `ProjectPathForms`, `PackageLayout`.

**Caveat:** reversing `:feature-home-impl` → `feature/home/impl` is **ambiguous**
when a path segment itself contains `-`. Prefer the filesystem relative path as
the canonical key for migrate plans.

## API (forma-core)

Package: `tools.forma.core.fleet`

```kotlin
// Package → directories
PackageLayout.segments("com.foo.bar")
PackageLayout.sourceDir("com.foo.bar") // src/main/kotlin/com/foo/bar
PackageLayout.sourceDir("com.foo.bar", SourceLanguage.JAVA)

// Path forms (includer-aligned)
ProjectPathForms.gradleProjectPath("feature/home/impl") // :feature-home-impl
ProjectPathForms.formaTargetPath("feature/home/impl") // :feature:home:impl

// Generate (GH #54)
LayoutGenerator.apply(
GenerateLayoutRequest(
moduleDir = modulePath,
packageName = "com.stepango.blockme.character.list.api",
language = SourceLanguage.KOTLIN,
createPlaceholder = true, // optional .gitkeep
),
)

// Check
LayoutChecker.checkPackageSourceDir(modulePath, "com.foo.bar") // empty = OK

// Migrate plan (no IO)
MigratePlanner.planRename(
PathRenamePlan("feature/home/impl", "feature/dashboard/impl"),
)
// → filesystemMoves + referenceRewrites (:gradle, :forma, target(), project())
```

### Using from a one-off script / future task

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.

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.

## What v1 ships vs follow-ups

**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)**

- 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)

- 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

## Agent skill

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

## Verify

```bash
source scripts/env-mac.sh
cd plugins && ./gradlew :core:test
# or full
./gradlew build
```
3 changes: 2 additions & 1 deletion docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ 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.
explicit structure, with tooling for large-scale change
([`FLEET-TOOLING.md`](FLEET-TOOLING.md) — check / generate / migrate).

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
22 changes: 22 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

Newest entries first.

## 2026-07-20 — F-084: fleet tooling check/generate/migrate v1

- **Ticket:** F-084 → `done` (v1 toolkit; follow-ups listed in `docs/FLEET-TOOLING.md`)
- **Branch:** `forma/F-084-fleet-tooling` (from origin/v2)
- **Skills/modes:** Grok Build `--mode full` design 503 + implement hang (no tree changes); Hermes finish path for pure-core + docs slice
- **Code (`plugins/core` `tools.forma.core.fleet`):**
- `PackageLayout` / `SourceLanguage` — packageName → `src/main/{kotlin|java}/…`
- `ProjectPathForms` — includer `:feature-home-impl` ↔ Forma `:feature:home:impl`
- `LayoutGenerator` plan/apply (GH #54) + optional `.gitkeep`
- `LayoutChecker` — missing package dir violations
- `MigratePlanner` — path rename + suggested reference rewrites (not AST)
- Unit tests: `FleetToolkitTest` (GH #54 package, idempotent apply, path forms, migrate)
- **Docs / teaching:**
- New `docs/FLEET-TOOLING.md` (modes, tool map, API, follow-ups)
- VISION principle 3 link; README + GETTING-STARTED; ARCHITECTURE audit stamp
- Agent skill `forma-fleet-tooling` + skills README + PROGRESSIVE-EXAMPLES row
- **Out of scope (documented follow-ups):** Gradle tasks, AST migrate, depgen finish, config-time package hook
- **Verify (real host, `source scripts/env-mac.sh`):**
- `plugins/ ./gradlew :core:test build` → **BUILD SUCCESSFUL** (78 tasks; core fleet unit tests green)
- **Blockers:** none for v1
- **Next:** F-085 full-tree principle audit

## 2026-07-20 — F-083: one external-deps house style

- **Ticket:** F-083 → `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 @@ -31,6 +31,7 @@ narrative tutorials when you want a **minimal buildable project per concept**.
| `testUtil` | 09 | 05 | platform skills |
| `androidTestUtil` | 09 | — | forma-android-targets |
| Type-owned target plugins (`navigationRes` Path B) | 10 | — | forma-target-plugins |
| 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 |

Expand Down
2 changes: 1 addition & 1 deletion docs/VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ README meta-build pitch; stated here so they cannot drift.
3. **Everything is explicit** — Structure and boundaries are declared, not implied
by hidden convention soup. Tradeoff: more configuration at the **type / project**
layer. Remedy: **tooling** so large-scale changes stay easy and reliable
(check / generate / migrate — includer, depgen, adapters, future fleet tools).
(check / generate / migrate — includer, adapters, [`FLEET-TOOLING.md`](FLEET-TOOLING.md)).

### Consequences (do not treat as separate products)

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 @@ -14,6 +14,7 @@ matrix rules, and progressive example pointers.
| [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 |
| [forma-fleet-tooling](forma-fleet-tooling.md) | check/generate/migrate layout + path forms (F-084) |
| [forma-bazel](forma-bazel.md) | Bazel adapter / sample |

**Hard rules agents must never violate**
Expand Down
54 changes: 54 additions & 0 deletions examples/agent-skills/forma-fleet-tooling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
name: forma-fleet-tooling
description: Check/generate/migrate package layout and path forms (F-084 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).

## 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)

## Rules

1. **One way:** use `PackageLayout` / `LayoutGenerator` / `LayoutChecker` / `MigratePlanner` — do not invent alternate path math.
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.
5. Still obey matrix / `impl` ↛ `impl` / no `androidLibrary` / type-owned plugins.

## Snippets

```kotlin
import tools.forma.core.fleet.*

// GH #54 generate
LayoutGenerator.apply(
GenerateLayoutRequest(
moduleDir = Path.of("feature/character/list/api"),
packageName = "com.stepango.blockme.character.list.api",
createPlaceholder = true,
),
)

// check
val violations = LayoutChecker.checkPackageSourceDir(moduleDir, packageName)
check(violations.isEmpty()) { violations.joinToString { it.message } }

// rename plan
val plan = MigratePlanner.planRename(
PathRenamePlan("feature/home/impl", "feature/dashboard/impl"),
)
// plan.filesystemMoves, plan.referenceRewrites
```

## Related skills

- [forma-includer-settings](forma-includer-settings.md) — discovery / include
- [forma-project-layout](forma-project-layout.md) — feature folders
- [forma-android-targets](forma-android-targets.md) / [forma-jvm-targets](forma-jvm-targets.md)
3 changes: 2 additions & 1 deletion plugins/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ publishing {
name.set("forma-core")
description.set(
"Forma core library (not a Gradle plugin): TargetType, RestrictionGraph, " +
"TargetValidator / ContentRule, TargetRegistry. Co-versioned with tools.forma.* plugins."
"TargetValidator / ContentRule, TargetRegistry, fleet check/generate/migrate helpers. " +
"Co-versioned with tools.forma.* plugins."
)
url.set("https://forma.tools/")
scm {
Expand Down
49 changes: 49 additions & 0 deletions plugins/core/src/main/java/tools/forma/core/fleet/LayoutChecker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package tools.forma.core.fleet

import java.nio.file.Files
import java.nio.file.Path

data class LayoutCheckViolation(
val path: String,
val message: String,
)

/**
* Check mode for package source layout (F-084). Pure relative to a module directory.
* Complements runtime [tools.forma.core.validation.ContentRule] checks.
*/
object LayoutChecker {

/**
* Verify that `src/main/{java|kotlin}/<package>` exists under [moduleDir].
*/
fun checkPackageSourceDir(
moduleDir: Path,
packageName: String,
language: SourceLanguage = SourceLanguage.KOTLIN,
): List<LayoutCheckViolation> {
val violations = mutableListOf<LayoutCheckViolation>()
val rel =
try {
PackageLayout.sourceDir(packageName, language)
} catch (e: IllegalArgumentException) {
return listOf(
LayoutCheckViolation(
path = packageName,
message = e.message ?: "invalid packageName",
),
)
}
val dir = moduleDir.resolve(rel)
if (!Files.isDirectory(dir)) {
violations +=
LayoutCheckViolation(
path = rel,
message =
"missing package source directory for '$packageName' " +
"(expected $rel under module)",
)
}
return violations
}
}
Loading
Loading