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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ Configuration-time performance: **F-017**
([`docs/CONFIGURATION-PERFORMANCE.md`](docs/CONFIGURATION-PERFORMANCE.md)).
forma-core API design: **F-020** ([`docs/forma-core-api.md`](docs/forma-core-api.md)).
forma-core extraction (restriction engine + TargetType + Android matrix kit): **F-021** done (`plugins/core`).
forma-core validation SPI + content rules (Gradle facade in `:validation`): **F-022** done.

Icons made by <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a>
from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
2 changes: 1 addition & 1 deletion TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi
|----|--------|-------|-------|
| F-020 | done | Design forma-core public API (types, restrictions, validation, target registry) | `docs/forma-core-api.md` — types, restriction graph, validator SPI, registry, coords, library-suffix decision |
| F-021 | done | Extract dependency-type / restriction engine into `forma-core` | `plugins/core` + TargetType/NameMatcher/RestrictionGraph + AndroidTargetTypes + AndroidRestrictionKit (distinct jvm.library vs android.library); facades preserved. Related GH #39 |
| F-022 | todo | Extract validation framework into `forma-core` | Keep Android validators as plugins |
| F-022 | done | Extract validation framework into `forma-core` | `plugins/core` TargetValidator + ContentRule; `:validation` facade; Android helpers call pure rules |
| F-023 | todo | Wire Android implementation as first consumer of forma-core | Sample still builds |
| F-024 | todo | Publish/coordinate coordinates: `tools.forma:core` vs android plugins | |

Expand Down
31 changes: 16 additions & 15 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,18 @@ Group/version (root `plugins/build.gradle.kts`): **`tools.forma` / `0.1.3`**.

```
┌────────────┐
│ core │ TargetType, NameMatcher, RestrictionGraph (F-021)
│ core │ TargetType, RestrictionGraph (F-021),
│ │ TargetValidator + ContentRule (F-022)
└─────▲──────┘
┌─────┴──────┐
│ target │ TargetTemplate, FormaTarget (parallel for compat)
│ target │ TargetTemplate; FormaTarget : TargetRef
└─────▲──────┘
┌───────────┼────────────┐
│ │ │
┌─────┴─────┐ ┌───┴────┐ │
│validation │ │ owners │ │
│validation │ │ owners │ │ validation = Gradle facade over core
└─────▲─────┘ └───▲────┘ │
│ │ │
┌─────┴─────┐ │ ┌─────┴─────┐
Expand All @@ -90,19 +91,19 @@ Group/version (root `plugins/build.gradle.kts`): **`tools.forma` / `0.1.3`**.
└─────┬─────┴────────────┘
┌─────┴─────┐
│ android │ user-facing DSL + AGP features (+ AndroidTargetTypes + matrix)
│ android │ DSL + AGP + AndroidTargetTypes + matrix + content helpers
└───────────┘
```

| Module | Plugin id | Depends on | Responsibility |
|--------|-----------|------------|----------------|
| `:core` | (library, not plugin) | (none / gradleApi compileOnly if needed) | `TargetType` / `TargetRef`, `NameMatcher`/`SuffixNameMatcher`, `RestrictionGraph`/`MutableRestrictionGraph`, `EdgeKind`, `RestrictionRule`. Pure engine. |
| `:target` | `tools.forma.target` | `gradleApi` | `TargetTemplate(suffix)`, `FormaTarget(project)` (compat facade; F-021 adds parallel core types) |
| `:validation` | `tools.forma.validation` | `:target`, `gradleApi` | Name validators, content validators, `ProjectValidationError` (F-022 will migrate to core) |
| `:core` | (library, not plugin) | (none) | Pure engine: `TargetType`/`TargetRef`, `NameMatcher`, `RestrictionGraph`, **`TargetValidator` / `dependencyTypeValidator` / `selfTypeValidator` / `AcceptAny`**, **`ContentRule` + No/OnlyResources/OnlyLayout**, `FormaValidationException`. Identity-cached factories (F-017). |
| `:target` | `tools.forma.target` | `:core`, `gradleApi` | `TargetTemplate(suffix)`; `FormaTarget(project)` implements `TargetRef` |
| `:validation` | `tools.forma.validation` | `:core`, `:target`, `gradleApi` | Thin Gradle facade: legacy `Validator`/`validator(TargetTemplate…)` → core; `ProjectValidationError`; `validateDirectoryContent` (FS listing) |
| `:owners` | `tools.forma.owners` | `gradleApi` | `Owner` / `Person` / `Team` / `NoOwner` |
| `:config` | `tools.forma.config` | `gradleApi` | `AndroidProjectSettings`, `FormaSettingsStore`, plugin/dep registration maps |
| `:deps` | `tools.forma.deps` | `:validation`, `:target`, `:config`, kotlin-dsl | `FormaDependency` model, `applyDependencies`, version-catalog generators |
| `:android` | `tools.forma.android` | `:core` + all of the above + **AGP** + Kotlin GP | Target DSL (`api`, `impl`, `androidLibrary`, …), feature appliers; owns `AndroidTargetTypes` + `AndroidRestrictionKit` (F-021) |
| `:deps` | `tools.forma.deps` | `:core`, `:validation`, `:target`, `:config`, kotlin-dsl | `FormaDependency` model, `applyDependencies`, version-catalog generators |
| `:android` | `tools.forma.android` | `:core` + all of the above + **AGP** + Kotlin GP | Target DSL; `AndroidTargetTypes` + `AndroidRestrictionKit` (F-021); content helpers call core `ContentRule` (F-022) |

`:android` compiles against **AGP 8.1.2** (aligned with sample runtime force in
`application/settings.gradle.kts` as of F-003). Keep `plugins/android` AGP
Expand Down Expand Up @@ -307,9 +308,9 @@ Aligned with `docs/VISION.md`: core must not assume Android/AGP/Dagger.

| Concern | Current home | forma-core? | Notes |
|---------|--------------|-------------|-------|
| Target type identity (`TargetTemplate` / suffix) | `:target` + `:core` (F-021) | **Yes** | Parallel `TargetType` in core; templates kept for compat in F-021. Registry in F-022/F-023. |
| Name + dep-type `Validator` | `:validation` | **Yes** | Keep framework; Android content rules as plugins |
| Content validators (`onlyAllowResources`, …) | `:android` + `:validation` helpers | **Split** | Generic dir checks → core; Android paths → android plugin |
| Target type identity (`TargetTemplate` / suffix) | `:target` + `:core` (F-021) | **Yes** | Parallel `TargetType` in core; templates kept for compat. Registry in F-023. |
| Name + dep-type `Validator` | `:core` + `:validation` facade (F-022) | **Yes** | Core `TargetValidator` + factories; legacy API re-exports |
| Content validators (`onlyAllowResources`, …) | core predicates + `:android` helpers (F-022) | **Split** | Pure `ContentRule` in core; Gradle listing + Android helpers in platform |
| Dependency model + apply | `:deps` | **Mostly yes** | Strip AGP-ish config features; catalog generators may stay tooling |
| Settings store | `:config` | **Split** | Generic `SettingsStore` / plugin registry → core; `AndroidProjectSettings` → android |
| Owners | `:owners` | **Optional / yes** | Platform-agnostic metadata |
Expand All @@ -323,9 +324,9 @@ Suggested extraction order (tickets F-020…F-024):
1. Document public API — **done (F-020):** [`forma-core-api.md`](forma-core-api.md)
(types, restriction graph, validator SPI, target registry, coords preview,
`library` suffix decision).
2. Create `plugins/core` + TargetType + RestrictionGraph + wire Android matrix from DEPENDENCY-MATRIX (F-021). Parallel types + facades; full move later.
3. Re-home `applyDependencies` project-validation path on core validators (F-022).
4. Leave `:android` as the first platform package implementing templates + AGP features (F-023).
2. Create `plugins/core` + TargetType + RestrictionGraph + wire Android matrix from DEPENDENCY-MATRIX — **done (F-021)**.
3. Validation SPI + content predicates in core; `:validation` thin facade — **done (F-022)**.
4. Wire Android DSL / registry as first consumer of core (F-023); sample stays green.
5. Coordinates: e.g. `tools.forma:core` vs `tools.forma.android` (F-024).

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

Newest entries first.

## 2026-07-13 — F-022 Extract validation framework into forma-core

- **Ticket:** F-022 → `done`
- **Branch:** `forma/F-022-validation-framework` (from `origin/v2`)
- **Skills/modes:** Grok Build `--mode full` (plan design → implement; implement hit max-turns); Hermes finished docs, OnlyLayout empty-list parity, builds, commit/PR
- **Core (`tools.forma.core.validation`):**
- `TargetValidator` SPI + `AcceptAny`
- `dependencyTypeValidator` / `selfTypeValidator` with **identity cache** (F-017)
- `FormaValidationException` (recognizable suffix messages)
- Pure `ContentRule` + `NoResourcesUnderMain` / `OnlyResourcesUnderMain` / `OnlyLayoutResources`
- Unit tests: `TargetValidatorTest`, `ContentRuleTest`
- **Facades:**
- `:validation` `Validator`/`validator(TargetTemplate…)` → core; still throws `ProjectValidationError`
- `FormaTarget` implements `TargetRef`; `:target`/`:deps` depend on `:core`
- Android `commonValidators.kt` lists dirs via Gradle, checks via core rules
- **Docs:** ARCHITECTURE graph/table/extraction; forma-core-api status + checklist; README Progress; TICKETS
- **Verify (OpenJDK 17 + env-mac.sh):**
- `plugins/`: `./gradlew :core:test` → **BUILD SUCCESSFUL**
- `plugins/`: `./gradlew build` → **BUILD SUCCESSFUL** (68 tasks)
- `application/`: `./gradlew :binary:assembleDebug` → **BUILD SUCCESSFUL** (578 tasks)
- **Commits/PRs:** this run — push + PR base `v2`
- **Blockers:** none (Grok implement max-turns; tree completed by Hermes verify/docs)
- **Next step:** F-023 Wire Android as first consumer of forma-core (registry / DSL)

## 2026-07-13 — Local maven publishing for plugin testing

- **Scope:** tooling for F-018 AGP/Gradle smoke tests + external consumers
Expand Down
29 changes: 15 additions & 14 deletions docs/forma-core-api.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# forma-core public API design (F-020)

Design-only ticket. This document is the contract for extraction tickets
**F-021…F-024**. It is grounded in the live plugins under `plugins/`
(`:target`, `:validation`, `:deps`, `:config`, `:owners`, `:android`) and in
[`ARCHITECTURE.md`](ARCHITECTURE.md) §6 + [`VISION.md`](VISION.md).
Design ticket contract for extraction **F-021…F-024**. Grounded in the live
plugins under `plugins/` and in [`ARCHITECTURE.md`](ARCHITECTURE.md) §6 +
[`VISION.md`](VISION.md).

**Status:** accepted design for workers. Code moves land in later tickets;
do **not** treat this file as already-implemented packages.
**Status:** accepted design. **Implemented so far:** F-021 (types +
restriction graph + Android kit), **F-022** (validation SPI + content
predicates in `plugins/core`; `:validation` is a Gradle facade). Registry /
DSL consumption remains **F-023**.

---

Expand Down Expand Up @@ -401,7 +402,7 @@ Bazel adapter (later): same registry + restriction graph; replace
|--------|--------|
| **F-020** (this doc) | Public API design only |
| **F-021** | Create `plugins/core` (or move packages); target types + **restriction graph** + wire Android matrix data; keep binary/API facades so sample builds |
| **F-022** | Move validation SPI, default suffix validators, content predicates; Android content helpers call core |
| **F-022** | **Done:** validation SPI, identity-cached factories, content predicates in `plugins/core`; Android helpers call core; `:validation` facade |
| **F-023** | Android DSL uses registry; delete duplicated allow-lists from individual `*.kt` entrypoints where safe; sample green |
| **F-024** | Publishing coordinates, README/Portal metadata, deprecate old plugin jars if merged |

Expand All @@ -413,15 +414,15 @@ Do not skip to JVM targets (F-030) until F-023 is done.

**Must ship in forma-core v1**

- [ ] `TargetType`, `TargetRef`, `NameMatcher` / `SuffixNameMatcher`
- [ ] `TargetRegistry` + `TargetRegistration` + `DefaultTargetRegistry`
- [ ] `RestrictionGraph` / `RestrictionRule` / `EdgeKind`
- [ ] `TargetValidator`, `AcceptAny`, `dependencyTypeValidator`, `selfTypeValidator`
- [ ] Content rule interfaces + `NoResourcesUnderMain` / `OnlyResourcesUnderMain`
- [ ] Validation error type + message helpers (suffix lists)
- [x] `TargetType`, `TargetRef`, `NameMatcher` / `SuffixNameMatcher` (F-021)
- [ ] `TargetRegistry` + `TargetRegistration` + `DefaultTargetRegistry` (F-023)
- [x] `RestrictionGraph` / `RestrictionRule` / `EdgeKind` (F-021)
- [x] `TargetValidator`, `AcceptAny`, `dependencyTypeValidator`, `selfTypeValidator` (F-022)
- [x] Content rule interfaces + `NoResourcesUnderMain` / `OnlyResourcesUnderMain` / `OnlyLayoutResources` (F-022)
- [x] Validation error type + message helpers (suffix lists) — core `FormaValidationException`; facade keeps `ProjectValidationError` (F-022)
- [ ] `SettingsStore`, `PluginBindingStore` (from today’s config interfaces)
- [ ] Dependency model types needed for project-edge validation
- [ ] Identity-cached validator factory (F-017 behavior)
- [x] Identity-cached validator factory (F-017 behavior) (F-022)

**Explicitly deferred**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,63 @@ package tools.forma.android.validation

import org.gradle.api.Project
import java.io.File
import tools.forma.validation.validateDirectoryContent
import tools.forma.core.validation.NoResourcesUnderMain
import tools.forma.core.validation.OnlyLayoutResources
import tools.forma.core.validation.OnlyResourcesUnderMain
import tools.forma.validation.buildException
// validateDirectoryContent kept in :validation as Gradle-coupled facade (core ContentRule is pure)

fun Project.disallowResources() = validateDirectoryContent(
dir = "./src/main",
errorMsg = "Please make sure this does not contain `res` directory"
) { files ->
files.filter(File::isDirectory)
.map { it.name }
.run { !contains("res") }
/**
* Gradle-coupled content helpers. Pure predicates live in core (F-022); listing + error wrapping
* uses the validation facade so behavior and messages for existing DSLs are unchanged.
*/

fun Project.disallowResources() {
val dir = "./src/main"
val files = file(dir).listFiles() ?: throw buildException(project.name, "'$dir' does not exists")
val names = files.filter(File::isDirectory).map { it.name }
val failure = NoResourcesUnderMain.check(names)
if (failure != null) {
throw buildException(
project.name,
"""$failure
|Current list of files in $dir:
|${files.joinToString("\n") { it.name }}
""".trimMargin()
)
}
}

fun Project.onlyAllowResources() = validateDirectoryContent(
dir = "./src/main",
errorMsg = "Please make sure this target only contains `res` folder in `src/main`"
) {
it.filter(File::isDirectory)
.run { size == 1 && first().name == "res" }
fun Project.onlyAllowResources() {
val dir = "./src/main"
val files = file(dir).listFiles() ?: throw buildException(project.name, "'$dir' does not exists")
val names = files.filter(File::isDirectory).map { it.name }
val failure = OnlyResourcesUnderMain.check(names)
if (failure != null) {
throw buildException(
project.name,
"""$failure
|Current list of files in $dir:
|${files.joinToString("\n") { it.name }}
""".trimMargin()
)
}
}

fun Project.onlyAllowLayouts() = validateDirectoryContent(
dir = "./src/main/res",
errorMsg = "Please make sure this target only contains `layout.*` folders in `src/main/res`"
) {
it.filter(File::isDirectory)
.all { it.name.startsWith("layout") }
fun Project.onlyAllowLayouts() {
val dir = "./src/main/res"
val files = file(dir).listFiles() ?: throw buildException(project.name, "'$dir' does not exists")
val names = files.filter(File::isDirectory).map { it.name }
val failure = OnlyLayoutResources.check(names)
if (failure != null) {
throw buildException(
project.name,
"""$failure
|Current list of files in $dir:
|${files.joinToString("\n") { it.name }}
""".trimMargin()
)
}
}

// Note: validateDirectoryContent remains in tools.forma.validation for any external Gradle-coupled usage.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package tools.forma.core.validation

/**
* Pure predicate for directory content validation (no Gradle APIs).
* Implementations receive relative file/dir names under the inspected root (e.g. "src/main" or "src/main/res").
* @return null if content is acceptable, otherwise a human-readable failure reason.
*/
fun interface ContentRule {
fun check(filesUnderRoot: List<String>): String?
}

/** Rejects any "res" directory under the inspected root (e.g. src/main). */
object NoResourcesUnderMain : ContentRule {
override fun check(filesUnderRoot: List<String>): String? =
if (filesUnderRoot.any { it == "res" }) {
"Please make sure this does not contain `res` directory"
} else {
null
}
}

/** Requires the inspected root to contain exactly one "res" directory and nothing else. */
object OnlyResourcesUnderMain : ContentRule {
override fun check(filesUnderRoot: List<String>): String? {
val dirs = filesUnderRoot
return if (dirs.size == 1 && dirs.first() == "res") {
null
} else {
"Please make sure this target only contains `res` folder in `src/main`"
}
}
}

/**
* For res/ inspection: every top-level entry under res must start with "layout".
* Empty list is OK (matches historical `list.all { startsWith("layout") }` on empty).
*/
object OnlyLayoutResources : ContentRule {
override fun check(filesUnderRoot: List<String>): String? {
val allLayouts = filesUnderRoot.all { it.startsWith("layout") }
return if (allLayouts) {
null
} else {
"Please make sure this target only contains `layout.*` folders in `src/main/res`"
}
}
}
Loading
Loading