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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ jobs:
with:
skills_directories: 'skills'

plugin-paths:
name: 📂 Plugin Paths
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Check for bare plugin paths
run: |
if grep -rnP '(?<![\w}/.-])(references|scripts)/' skills agents; then
echo "::error::Prefix plugin file paths in skills/ and agents/ with \${CLAUDE_PLUGIN_ROOT}/"
exit 1
fi

plugin-validate:
name: 📦 Plugin Validation
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Documentation drifts when an asset changes and the docs describing it do not. Up

## Checks

CI runs markdownlint with `config/custom.markdownlint.jsonc`, cspell with `config/cspell.json`, the validator's analyze, format, and tests from `scripts/`, skills lint on `skills/`, and `claude plugin validate .`. `CHANGELOG.md` and `references/ffca/` are excluded from markdownlint. A weekly `reference_drift` workflow runs `sync_reference.dart --check` and fails when the mirror is stale.
CI runs markdownlint with `config/custom.markdownlint.jsonc`, cspell with `config/cspell.json`, the validator's analyze, format, and tests from `scripts/`, skills lint on `skills/`, a check that every plugin file path in `skills/` and `agents/` starts with `${CLAUDE_PLUGIN_ROOT}/`, and `claude plugin validate .`. `CHANGELOG.md` and `references/ffca/` are excluded from markdownlint. A weekly `reference_drift` workflow runs `sync_reference.dart --check` and fails when the mirror is stale.

## Commits

Expand Down
20 changes: 10 additions & 10 deletions agents/ffca-layer-auditor.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,22 @@ From the repo root, run the validator in full-graph mode and treat every line it
dart run ${CLAUDE_PLUGIN_ROOT}/scripts/validate_layers.dart --all
```

This is the authoritative check for the pubspec dependency graph: package naming under `features/`, the layer dependency rules, the no-dependency-on-an-app rule, workspace-wide cycles, feature ownership (a data package depends on its own domain, a data package has a domain in its feature, a domain package never depends on the Flutter SDK), and imports and barrels (each feature package has a primary barrel, no import reaches past another package's barrels, another feature's presentation is imported through a dedicated barrel, a data barrel never exports DTOs or mappers, every declared workspace dependency is imported, and every imported workspace package is declared and allowed by the layer rules). It prints each finding with its rule ID, severity (`✗` error, `⚠` warning), and fix, then a summary line with the error, warning, and suppressed counts. It exits 2 if any error is found, and 0 when there are only warnings or nothing at all. An error makes the package `fail`; a warning makes it `warn`. A finding suppressed with an `# ffca-ignore:` comment is an accepted exception: do not report it as a violation, but list the suppression and its reason in the package's notes. Do not re-derive these checks by hand; the script is the source of truth for them. Read `references/ffca/project_structure.md`, sections *Dependency rules* and *Naming conventions*, only if you need to explain a finding.
This is the authoritative check for the pubspec dependency graph: package naming under `features/`, the layer dependency rules, the no-dependency-on-an-app rule, workspace-wide cycles, feature ownership (a data package depends on its own domain, a data package has a domain in its feature, a domain package never depends on the Flutter SDK), and imports and barrels (each feature package has a primary barrel, no import reaches past another package's barrels, another feature's presentation is imported through a dedicated barrel, a data barrel never exports DTOs or mappers, every declared workspace dependency is imported, and every imported workspace package is declared and allowed by the layer rules). It prints each finding with its rule ID, severity (`✗` error, `⚠` warning), and fix, then a summary line with the error, warning, and suppressed counts. It exits 2 if any error is found, and 0 when there are only warnings or nothing at all. An error makes the package `fail`; a warning makes it `warn`. A finding suppressed with an `# ffca-ignore:` comment is an accepted exception: do not report it as a violation, but list the suppression and its reason in the package's notes. Do not re-derive these checks by hand; the script is the source of truth for them. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, sections *Dependency rules* and *Naming conventions*, only if you need to explain a finding.

## Step 2: source-level checks (qualitative)

The script validates the pubspec graph and the import and export directives in `lib/` and `bin/`. These checks need judgment or a deeper read of the source, so do them by reading and grepping the tree. Cite the reference section for each.

1. **Package inventory and classification.** Enumerate every package under `apps/`, `features/`, `shared/`. Classify each by folder, name, and inferred type: full feature, headless feature (domain plus data, no presentation), presentation-only feature (a `_presentation` with no domain or data sibling), or shared package. All four are legitimate; a missing sibling is a signal, not a violation. See `references/ffca/overview.md`, sections *Features*, *Headless features*, *Presentation-only features*, *Shared libraries*.
2. **Barrel scope.** The validator checks that barrels exist and that imports go through them. Check what they expose: a feature with several independent entry points has a subfeature barrel for each, no barrel re-exports a private symbol (`rg -n "^export '.*/_" features shared apps`), and a barrel made for another feature exports only that widget, with transitive imports that do not reach the feature's modules or screens. See `references/ffca/presentation.md`, sections *Subfeature barrel files* and *Sharing a widget across features*.
3. **DTO leakage.** Generated or transport types (`*.g.dart`, `*.freezed.dart`, anything under a `dtos/` folder) must not be imported outside the data package that owns them. The validator already flags a data barrel that exports them and any import that reaches past a barrel, so look for the remaining path: a DTO or generated type in a public signature, such as a repository method. Detect with `rg -n "import .*\.(g|freezed)\.dart'" features shared apps` and check the importing package owns the source. See `references/ffca/data.md`, section *DTOs and mappers*.
4. **Command and Query necessity.** A `Query` or `Command` class in a `*_domain` package is justified only when it combines two or more repositories or removes duplication across Blocs. A single-repository pass-through is an anti-pattern. Open each `*_query.dart` / `*_command.dart` and count injected repositories. Also check the verbs: commands expose `execute`, queries expose `get` or `watch`, and neither is a callable class. Classes named `*UseCase` are a naming finding. See `references/ffca/domain.md`, section *Business rules*, and `references/ffca/faq.md`, section *Should we use callable classes for Commands and Queries?*
5. **Module entry points.** App route bindings should instantiate a feature `*Module`, not a screen or page widget directly, so dependencies stay explicit. Inspect the app's router and verify each route builds a `*Module`. See `references/ffca/presentation.md`, section *The module*, and `references/ffca/navigation.md`.
6. **Misplaced packages.** Volatile or app-specific business logic sitting in `shared/`, or a generic, pub-publishable utility sitting in `features/`. Apply the decision rule and component table in `references/ffca/overview.md`, section *Shared libraries*.
7. **High fan-in domains.** A domain many features depend on may be doing too much. Note it for review against `references/ffca/domain.md`, section *Composing features*.
1. **Package inventory and classification.** Enumerate every package under `apps/`, `features/`, `shared/`. Classify each by folder, name, and inferred type: full feature, headless feature (domain plus data, no presentation), presentation-only feature (a `_presentation` with no domain or data sibling), or shared package. All four are legitimate; a missing sibling is a signal, not a violation. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, sections *Features*, *Headless features*, *Presentation-only features*, *Shared libraries*.
2. **Barrel scope.** The validator checks that barrels exist and that imports go through them. Check what they expose: a feature with several independent entry points has a subfeature barrel for each, no barrel re-exports a private symbol (`rg -n "^export '.*/_" features shared apps`), and a barrel made for another feature exports only that widget, with transitive imports that do not reach the feature's modules or screens. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, sections *Subfeature barrel files* and *Sharing a widget across features*.
3. **DTO leakage.** Generated or transport types (`*.g.dart`, `*.freezed.dart`, anything under a `dtos/` folder) must not be imported outside the data package that owns them. The validator already flags a data barrel that exports them and any import that reaches past a barrel, so look for the remaining path: a DTO or generated type in a public signature, such as a repository method. Detect with `rg -n "import .*\.(g|freezed)\.dart'" features shared apps` and check the importing package owns the source. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/data.md`, section *DTOs and mappers*.
4. **Command and Query necessity.** A `Query` or `Command` class in a `*_domain` package is justified only when it combines two or more repositories or removes duplication across Blocs. A single-repository pass-through is an anti-pattern. Open each `*_query.dart` / `*_command.dart` and count injected repositories. Also check the verbs: commands expose `execute`, queries expose `get` or `watch`, and neither is a callable class. Classes named `*UseCase` are a naming finding. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/domain.md`, section *Business rules*, and `${CLAUDE_PLUGIN_ROOT}/references/ffca/faq.md`, section *Should we use callable classes for Commands and Queries?*
5. **Module entry points.** App route bindings should instantiate a feature `*Module`, not a screen or page widget directly, so dependencies stay explicit. Inspect the app's router and verify each route builds a `*Module`. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, section *The module*, and `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md`.
6. **Misplaced packages.** Volatile or app-specific business logic sitting in `shared/`, or a generic, pub-publishable utility sitting in `features/`. Apply the decision rule and component table in `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, section *Shared libraries*.
7. **High fan-in domains.** A domain many features depend on may be doing too much. Note it for review against `${CLAUDE_PLUGIN_ROOT}/references/ffca/domain.md`, section *Composing features*.

8. **Split routing tables.** If the app's routes live in more than one file, every file but the entry library must begin with `part of`. A per-feature route file that is a separate library is silently dropped by `go_router_builder`, and the build still succeeds. Detect with `rg -L "^part of" apps/*/lib/**/[a-z_]*routes.dart`. See `references/ffca/navigation.md`, section *Splitting the routing table across files*.
9. **Deferred loading reachability.** For each feature the app imports with a `deferred as` prefix, check that no non-deferred import path from the app reaches the same package. An eager edge, typically one presentation package importing another, silently cancels the code splitting. Advisory on apps that ship only to iOS and Android. See `references/ffca/project_structure.md`, section *Deferred loading*.
8. **Split routing tables.** If the app's routes live in more than one file, every file but the entry library must begin with `part of`. A per-feature route file that is a separate library is silently dropped by `go_router_builder`, and the build still succeeds. Detect with `rg -L "^part of" apps/*/lib/**/[a-z_]*routes.dart`. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md`, section *Splitting the routing table across files*.
9. **Deferred loading reachability.** For each feature the app imports with a `deferred as` prefix, check that no non-deferred import path from the app reaches the same package. An eager edge, typically one presentation package importing another, silently cancels the code splitting. Advisory on apps that ship only to iOS and Android. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Deferred loading*.

## Step 3: report

Expand Down
22 changes: 11 additions & 11 deletions skills/ffca-architecture/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ The detection signal is a `features/` folder whose packages follow the `{feature

## Workflow: deciding where code lives

1. Read `references/ffca/overview.md`, section *The structure*, to place the work in one of the three top-level folders: `apps/`, `features/`, `shared/`.
2. To decide between a feature and a shared package, apply the two-question decision rule in `references/ffca/overview.md`, section *Shared libraries* (a business capability an app composes goes to `features/`; something you could publish to pub.dev and a stranger would use unchanged goes to `shared/`). Use its component table to settle edge cases.
3. To decide which layers a feature needs, read `references/ffca/overview.md`, sections *Headless features* and *Presentation-only features*. A feature with no screens stays domain plus data and can grow a presentation package later with no structural change. A feature that only composes other features' domains into a screen has a presentation package and nothing else.
4. Before naming any package, read `references/ffca/project_structure.md`, section *Naming conventions*. The names are mechanically validated by the hook, so they are not optional.
5. To place a file inside a package, read `references/ffca/project_structure.md`, section *Layer subfolders*, for the per-layer folder map (`models/`, `repositories/`, `use_cases/`, `data_sources/`, `dtos/`, `mappers/`, and the per-screen `bloc/`, `views/`, module layout).
1. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, section *The structure*, to place the work in one of the three top-level folders: `apps/`, `features/`, `shared/`.
2. To decide between a feature and a shared package, apply the two-question decision rule in `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, section *Shared libraries* (a business capability an app composes goes to `features/`; something you could publish to pub.dev and a stranger would use unchanged goes to `shared/`). Use its component table to settle edge cases.
3. To decide which layers a feature needs, read `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, sections *Headless features* and *Presentation-only features*. A feature with no screens stays domain plus data and can grow a presentation package later with no structural change. A feature that only composes other features' domains into a screen has a presentation package and nothing else.
4. Before naming any package, read `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Naming conventions*. The names are mechanically validated by the hook, so they are not optional.
5. To place a file inside a package, read `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Layer subfolders*, for the per-layer folder map (`models/`, `repositories/`, `use_cases/`, `data_sources/`, `dtos/`, `mappers/`, and the per-screen `bloc/`, `views/`, module layout).

## Vocabulary: Command and Query, not use case

The classes in `use_cases/` are named **Command** (mutates, `execute`) and **Query** (reads, `get` or `watch`). Use those words when you talk about them and when you name them. The folder keeps the conventional `use_cases/` name so the layout matches other clean architecture projects, but the term "use case" is not used for the classes. Read `references/ffca/domain.md`, section *Business rules*.
The classes in `use_cases/` are named **Command** (mutates, `execute`) and **Query** (reads, `get` or `watch`). Use those words when you talk about them and when you name them. The folder keeps the conventional `use_cases/` name so the layout matches other clean architecture projects, but the term "use case" is not used for the classes. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/domain.md`, section *Business rules*.

## Before you wire dependencies

Read `references/ffca/project_structure.md`, section *Dependency rules*, before adding a dependency on another workspace package to a pubspec. That section's table is the whole policy, and it is what `scripts/validate_layers.dart` implements. The hook enforces it on every pubspec edit and blocks the edit on a violation, so confirm the direction first:
Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Dependency rules*, before adding a dependency on another workspace package to a pubspec. That section's table is the whole policy, and it is what `${CLAUDE_PLUGIN_ROOT}/scripts/validate_layers.dart` implements. The hook enforces it on every pubspec edit and blocks the edit on a violation, so confirm the direction first:

- apps depend on features and shared
- shared depends only on external packages
Expand All @@ -50,16 +50,16 @@ Two absences carry as much weight as the rows: no presentation package depends o

An app can load a feature on demand with a `deferred as` import, which is what splits web bundles and backs Android deferred components. This only works if the deferred package is not *also* reachable from the app through a non-deferred path. If `product_presentation` imports `cart_presentation` eagerly, deferring `cart` from the router achieves nothing.

The failure is silent: nothing breaks, no analyzer warning fires, and the bundle just stops splitting. Treat it as a rule, not a review note. Read `references/ffca/project_structure.md`, section *Deferred loading*. On an app shipping only to iOS and Android the AOT snapshot contains the whole program anyway, so this is informational there.
The failure is silent: nothing breaks, no analyzer warning fires, and the bundle just stops splitting. Treat it as a rule, not a review note. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Deferred loading*. On an app shipping only to iOS and Android the AOT snapshot contains the whole program anyway, so this is informational there.

## Anti-patterns to reject

These are the violations the architecture exists to prevent. Read the cited sections for the rationale:

- Presentation importing a data layer. Depend on the domain and go through its repository interface. See `references/ffca/project_structure.md`, section *Dependency rules*.
- A shared package depending on a feature. See `references/ffca/overview.md`, section *Shared libraries*.
- Presentation importing a data layer. Depend on the domain and go through its repository interface. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Dependency rules*.
- A shared package depending on a feature. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, section *Shared libraries*.
- Business logic living in `shared/`. Volatile, app-specific logic belongs in a feature domain. See the component table in the same section.
- A widget in `ui_kit` that needs a repository. It cannot compile there, because shared packages depend on external packages only. Split it: the presentational view goes to `ui_kit`, the stateful part stays in its feature. See `references/ffca/presentation.md`, section *Where the widget should live*.
- A widget in `ui_kit` that needs a repository. It cannot compile there, because shared packages depend on external packages only. Split it: the presentational view goes to `ui_kit`, the stateful part stays in its feature. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, section *Where the widget should live*.

## Where to go next

Expand Down
Loading
Loading