From f154d0f6ff3794ea8614d1b9a1eb97a452472f08 Mon Sep 17 00:00:00 2001 From: "unicoderbot[bot]" <269805761+unicoderbot[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:17:23 +0000 Subject: [PATCH] fix: prefix plugin file paths with ${CLAUDE_PLUGIN_ROOT} Closes #37 Co-authored-by: marcossevilla --- .github/workflows/ci.yaml | 12 ++++++++++++ AGENTS.md | 2 +- agents/ffca-layer-auditor.md | 20 ++++++++++---------- skills/ffca-architecture/SKILL.md | 22 +++++++++++----------- skills/ffca-audit/SKILL.md | 2 +- skills/ffca-cross-feature/SKILL.md | 24 ++++++++++++------------ skills/ffca-feature/SKILL.md | 26 +++++++++++++------------- skills/ffca-routing/SKILL.md | 20 ++++++++++---------- 8 files changed, 70 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5ecc3ee..7c59bcb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 '(?`); the stored summary holds ids (`CartSummary` with `List`). The repository reads and writes summaries and only takes the full object when one is being created. Read `references/ffca/domain.md`, section *Storing data*, and adapt the shapes in `references/code_templates/domain_templates.md`. -2. **Combine repositories with a Query.** To assemble a populated model from two features, add a `Query` in the consuming feature's domain that takes both repository interfaces. Read `references/ffca/domain.md`, section *Reading the populated object*. Keep the Query in the consumer, never in the provider: the cart feature never learns how products are stored, and the product feature never learns that carts exist. -3. **Normalize inside the repository, not a Query.** When an API returns nested objects belonging to another feature, split them inside the data repository, which may depend on another feature's domain interface. Read `references/ffca/faq.md`, section *Dealing with nested objects*. The DTO-to-domain mapping is duplicated in the consuming data package on purpose: DRY is traded for bounded contexts here deliberately. +1. **Loose coupling by id: the Summary pattern.** When a feature stores references to another feature's models, store ids only. The read model holds full objects (`Cart` with `List`); the stored summary holds ids (`CartSummary` with `List`). The repository reads and writes summaries and only takes the full object when one is being created. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/domain.md`, section *Storing data*, and adapt the shapes in `${CLAUDE_PLUGIN_ROOT}/references/code_templates/domain_templates.md`. +2. **Combine repositories with a Query.** To assemble a populated model from two features, add a `Query` in the consuming feature's domain that takes both repository interfaces. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/domain.md`, section *Reading the populated object*. Keep the Query in the consumer, never in the provider: the cart feature never learns how products are stored, and the product feature never learns that carts exist. +3. **Normalize inside the repository, not a Query.** When an API returns nested objects belonging to another feature, split them inside the data repository, which may depend on another feature's domain interface. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/faq.md`, section *Dealing with nested objects*. The DTO-to-domain mapping is duplicated in the consuming data package on purpose: DRY is traded for bounded contexts here deliberately. -Do not call these classes "use cases". They are Commands and Queries. See `references/ffca/domain.md`, section *Business rules*. +Do not call these classes "use cases". They are Commands and Queries. See `${CLAUDE_PLUGIN_ROOT}/references/ffca/domain.md`, section *Business rules*. ## Sharing a widget A widget that owns a `Cubit` is not a separate feature. It lives in its own feature's presentation package and is exported through a barrel. Two ways to get it onto another feature's screen, and the choice is about who places it. -**Direct import behind a narrow barrel.** Three conditions, all from `references/ffca/presentation.md`, section *Sharing a widget across features*: +**Direct import behind a narrow barrel.** Three conditions, all from `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, section *Sharing a widget across features*: - The widget gets its own barrel. Consumers import `package:cart_presentation/cart_badge.dart`, never the primary `cart_presentation.dart` barrel. - The barrel stays narrow. Its transitive imports must not reach the feature's modules or screens. Going through the primary barrel costs the consumer every screen and Cubit the feature owns, and it breaks deferred loading downstream. - No cycles. If two features each need a widget from the other, extract the shared part downward into a third package instead of importing sideways. -**A slot the app fills.** The consuming feature never learns the other feature exists: it declares a nullable `Widget` parameter and the app supplies it. Same inversion as navigation. Name the slot for its position (`trailingAction`), default it to nothing, and stop at two slots per module. Read `references/ffca/presentation.md`, section *Or let the app place it*. +**A slot the app fills.** The consuming feature never learns the other feature exists: it declares a nullable `Widget` parameter and the app supplies it. Same inversion as navigation. Name the slot for its position (`trailingAction`), default it to nothing, and stop at two slots per module. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, section *Or let the app place it*. -**Choosing.** Use a slot when the widget is app chrome, such as a cart badge in an app bar, which belongs to the shell rather than to the screen around it. Use a direct import when the widget is genuinely part of what the consuming screen *is*. Read `references/ffca/presentation.md`, section *Choosing between them*. +**Choosing.** Use a slot when the widget is app chrome, such as a cart badge in an app bar, which belongs to the shell rather than to the screen around it. Use a direct import when the widget is genuinely part of what the consuming screen *is*. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, section *Choosing between them*. ## Where a shared widget should live -When the same widget is wanted in more than one feature, work through `references/ffca/presentation.md`, section *Where the widget should live*, in order: +When the same widget is wanted in more than one feature, work through `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, section *Where the widget should live*, in order: 1. **Does it need a repository or a domain type?** If not, it is a pure presentational component and belongs in `ui_kit`. This test is mechanical, not stylistic: shared packages depend on external packages only, so a widget that needs a repository cannot compile there. 2. **If it does, split it.** The view taking a plain count goes in `ui_kit`; the Cubit plus that view stays in its feature behind its own barrel. A widget that genuinely gets reused across features is usually a presentational leaf with the stateful part left behind, which means the pressure was pointing at `ui_kit` all along. ## When a Query is not enough: compose a feature -If multi-domain logic grows its own models, screens, or lifecycle, create a feature for it. Read `references/ffca/overview.md`, sections *Features* and *Presentation-only features*. A feature that composes several other domains into a screen without owning business logic is a presentation-only feature: a `{name}_presentation` package with no domain or data sibling, depending on the other features' domains. +If multi-domain logic grows its own models, screens, or lifecycle, create a feature for it. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, sections *Features* and *Presentation-only features*. A feature that composes several other domains into a screen without owning business logic is a presentation-only feature: a `{name}_presentation` package with no domain or data sibling, depending on the other features' domains. ## Identity versus entity -Auth and user profile are separate domains, not one. Read `references/ffca/faq.md`, section *How do I handle auth and user profiles?* Keep `AuthUser` in `auth_domain` and `UserProfile` in `user_profile_domain`, and glue them with a Query in the consuming domain, so a new profile field never forces a change to auth. +Auth and user profile are separate domains, not one. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/faq.md`, section *How do I handle auth and user profiles?* Keep `AuthUser` in `auth_domain` and `UserProfile` in `user_profile_domain`, and glue them with a Query in the consuming domain, so a new profile field never forces a change to auth. diff --git a/skills/ffca-feature/SKILL.md b/skills/ffca-feature/SKILL.md index 47e2037..1d97967 100644 --- a/skills/ffca-feature/SKILL.md +++ b/skills/ffca-feature/SKILL.md @@ -22,8 +22,8 @@ Workflow for building a feature in an FFCA monorepo. Conventions and code shapes ## Before you start 1. Confirm the repo is FFCA-shaped (a `features/` folder with `{feature}_{layer}` packages). If not, stop and use the layered-architecture skill. -2. Read `references/ffca/overview.md`, section *Feature layers*, for the layer model, and `references/ffca/project_structure.md`, section *Naming conventions*, for package names (the hook validates them). -3. Check for a pre-generated API client first. If the backend ships one OpenAPI/Swagger definition, read `references/ffca/faq.md`, section *What if the backend has one large OpenAPI or Swagger definition for every endpoint?* The client belongs in `shared/`, and each feature's data layer consumes it. +2. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, section *Feature layers*, for the layer model, and `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Naming conventions*, for package names (the hook validates them). +3. Check for a pre-generated API client first. If the backend ships one OpenAPI/Swagger definition, read `${CLAUDE_PLUGIN_ROOT}/references/ffca/faq.md`, section *What if the backend has one large OpenAPI or Swagger definition for every endpoint?* The client belongs in `shared/`, and each feature's data layer consumes it. ## Build order: domain, then data, then presentation @@ -31,16 +31,16 @@ Scaffold each package with the Very Good CLI (`dart_package` for domain and data ### 1. Domain (`{feature}_domain`, Dart package) -Read `references/ffca/domain.md` and adapt `references/code_templates/domain_templates.md`: +Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/domain.md` and adapt `${CLAUDE_PLUGIN_ROOT}/references/code_templates/domain_templates.md`: - Models in `models/`: plain Dart with value equality. Do not suffix them with `Model` or `Entity`. - Repository interfaces in `repositories/`: `abstract interface class`, one per feature, named for the feature's own aggregate. A feature's domain never defines another feature's repository. -- Commands and Queries in `use_cases/`: add one only when you combine multiple repositories or repeat the same work across Blocs. A pass-through to a single repository does not need a class at all. Name them `{Verb}{Thing}Command` and `{Get,Watch}{Thing}Query`, and use `execute` for commands, `get` or `watch` for queries. Do not call these "use cases" in names or docs; the folder keeps that name, the classes do not. See section *Business rules* and `references/ffca/faq.md`, section *Should we use callable classes for Commands and Queries?* +- Commands and Queries in `use_cases/`: add one only when you combine multiple repositories or repeat the same work across Blocs. A pass-through to a single repository does not need a class at all. Name them `{Verb}{Thing}Command` and `{Get,Watch}{Thing}Query`, and use `execute` for commands, `get` or `watch` for queries. Do not call these "use cases" in names or docs; the folder keeps that name, the classes do not. See section *Business rules* and `${CLAUDE_PLUGIN_ROOT}/references/ffca/faq.md`, section *Should we use callable classes for Commands and Queries?* - Add the primary barrel `{feature}_domain.dart`. ### 2. Data (`{feature}_data`, Dart package) -Read `references/ffca/data.md` and adapt `references/code_templates/data_templates.md`: +Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/data.md` and adapt `${CLAUDE_PLUGIN_ROOT}/references/code_templates/data_templates.md`: - Data sources in `data_sources/`, each with a `dtos/` subfolder. A DTO never escapes the package. - Mappers in `mappers/`. Prefer a `Converter` subclass, which keeps the mapping in one named, testable place; extension methods such as `toDomain()` and generators like `auto_mappr` are also fine. One converter per source and per direction, so a feature reading from a database and an API has a `DbToDomain...` and an `ApiToDomain...`. @@ -49,27 +49,27 @@ Read `references/ffca/data.md` and adapt `references/code_templates/data_templat ### 3. Presentation (`{feature}_presentation`, Flutter package) -Read `references/ffca/presentation.md` and adapt `references/code_templates/presentation_templates.md`: +Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md` and adapt `${CLAUDE_PLUGIN_ROOT}/references/code_templates/presentation_templates.md`: - Per screen: a `{screen}/bloc/` Cubit with sealed states (Initial, Loading, Loaded, Error), a `{screen}/views/` screen that switches on state, and a `{screen}/{screen}_module.dart` that wires dependencies and exposes navigation callbacks. -- The module constructs its dependencies with `Provider`. Read `references/ffca/presentation.md`, section *The module*: standardizing on one DI package is deliberate, so that every module in every feature reads the same way. -- Add subfeature barrels per independent entry point plus the primary barrel `{feature}_presentation.dart`. Read `references/ffca/presentation.md`, section *Subfeature barrel files*, for why this matters to deferred imports. +- The module constructs its dependencies with `Provider`. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, section *The module*: standardizing on one DI package is deliberate, so that every module in every feature reads the same way. +- Add subfeature barrels per independent entry point plus the primary barrel `{feature}_presentation.dart`. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, section *Subfeature barrel files*, for why this matters to deferred imports. - For navigation wiring, use the `ffca-routing` skill. ## Feature shapes other than all three layers -- **Headless feature**: domain plus data, no UI. Read `references/ffca/overview.md`, section *Headless features*. When a screen is needed later, add a `{feature}_presentation` package with no change to the existing packages. If the data layer is backend-specific, name it `{feature}_data_{backend}`, for example `auth_data_firebase`. -- **Presentation-only feature**: a presentation package and nothing else, existing to compose other features' domains into a screen. Read `references/ffca/overview.md`, section *Presentation-only features*. The layout and naming do not change: it sits at `features/{name}/{name}_presentation` like any other presentation package, and the absence of siblings is the signal. Nothing needs declaring. +- **Headless feature**: domain plus data, no UI. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, section *Headless features*. When a screen is needed later, add a `{feature}_presentation` package with no change to the existing packages. If the data layer is backend-specific, name it `{feature}_data_{backend}`, for example `auth_data_firebase`. +- **Presentation-only feature**: a presentation package and nothing else, existing to compose other features' domains into a screen. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/overview.md`, section *Presentation-only features*. The layout and naming do not change: it sits at `features/{name}/{name}_presentation` like any other presentation package, and the absence of siblings is the signal. Nothing needs declaring. ## Reserving a slot for a widget you do not own -When a screen needs to display something owned by another feature, the module can declare a slot the app fills, instead of importing that feature. Give the parameter a nullable `Widget` type, default it to nothing, and name it for its position (`trailingAction`), never for the widget you expect (`cartBadgeBuilder` leaks the other feature straight back in). More than two slots on one module means the composition belongs in an app-owned shell. Read `references/ffca/presentation.md`, section *Or let the app place it*, and use the `ffca-cross-feature` skill to choose between a slot and a direct import. +When a screen needs to display something owned by another feature, the module can declare a slot the app fills, instead of importing that feature. Give the parameter a nullable `Widget` type, default it to nothing, and name it for its position (`trailingAction`), never for the widget you expect (`cartBadgeBuilder` leaks the other feature straight back in). More than two slots on one module means the composition belongs in an app-owned shell. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/presentation.md`, section *Or let the app place it*, and use the `ffca-cross-feature` skill to choose between a slot and a direct import. ## Pubspecs and dependencies -When you add dependencies between packages, follow `references/ffca/project_structure.md`, section *Dependency rules*. The pubspec hook blocks edits that violate a layer rule and tells you the fix, so set the direction correctly the first time: data depends on its own domain, presentation depends on domains and never on a data layer, neither depends on an app. +When you add dependencies between packages, follow `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Dependency rules*. The pubspec hook blocks edits that violate a layer rule and tells you the fix, so set the direction correctly the first time: data depends on its own domain, presentation depends on domains and never on a data layer, neither depends on an app. -If the app defers this feature's import, read `references/ffca/project_structure.md`, section *Deferred loading*, before adding a dependency on another feature's presentation package. An eager edge from a deferred package silently cancels the code splitting downstream. +If the app defers this feature's import, read `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Deferred loading*, before adding a dependency on another feature's presentation package. An eager edge from a deferred package silently cancels the code splitting downstream. ## Tests diff --git a/skills/ffca-routing/SKILL.md b/skills/ffca-routing/SKILL.md index 34424bb..8b4333e 100644 --- a/skills/ffca-routing/SKILL.md +++ b/skills/ffca-routing/SKILL.md @@ -21,20 +21,20 @@ Workflow for wiring navigation in an FFCA monorepo. The patterns and code shapes ## The core constraint -Features are isolated, so a feature cannot know the app's global routing table. It never imports the app's router or another feature's routes. Navigation is a dependency the app layer injects: the app decides *what happens next*, the feature only detects *when* it is needed. Read the intro of `references/ffca/navigation.md`, then follow the pattern that fits the feature's depth. +Features are isolated, so a feature cannot know the app's global routing table. It never imports the app's router or another feature's routes. Navigation is a dependency the app layer injects: the app decides *what happens next*, the feature only detects *when* it is needed. Read the intro of `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md`, then follow the pattern that fits the feature's depth. ## Choose the injection pattern -1. **Shallow widget trees: callbacks on the module.** The module constructor takes typed callbacks (`onProductTapped`, `onCheckoutStarted`) and the app supplies them. Read `references/ffca/navigation.md`, section *Simple features: pass callbacks directly*, and adapt the module example in `references/code_templates/presentation_templates.md`. -2. **Deep widget trees: a navigation interface.** Define a plain Dart class in the feature's presentation package holding every navigation action, require it in the module, and provide it to the subtree with `Provider.value`. Deep widgets then call `context.read().onProductTapped(id)` with no prop drilling. Read `references/ffca/navigation.md`, section *Deep trees: define a navigation interface*. +1. **Shallow widget trees: callbacks on the module.** The module constructor takes typed callbacks (`onProductTapped`, `onCheckoutStarted`) and the app supplies them. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md`, section *Simple features: pass callbacks directly*, and adapt the module example in `${CLAUDE_PLUGIN_ROOT}/references/code_templates/presentation_templates.md`. +2. **Deep widget trees: a navigation interface.** Define a plain Dart class in the feature's presentation package holding every navigation action, require it in the module, and provide it to the subtree with `Provider.value`. Deep widgets then call `context.read().onProductTapped(id)` with no prop drilling. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md`, section *Deep trees: define a navigation interface*. Whichever you pick, the callback at the module boundary is the compile-time-safe cross-feature contract. ## Wire routes in the app layer with go_router_builder -Use `go_router` with `go_router_builder`. The app defines `@TypedGoRoute` and `GoRouteData` classes that instantiate feature modules and wire their callbacks to typed route navigation. Adapt the `GoRouteData` template in `references/code_templates/presentation_templates.md`. +Use `go_router` with `go_router_builder`. The app defines `@TypedGoRoute` and `GoRouteData` classes that instantiate feature modules and wire their callbacks to typed route navigation. Adapt the `GoRouteData` template in `${CLAUDE_PLUGIN_ROOT}/references/code_templates/presentation_templates.md`. -Two hard rules from `references/ffca/navigation.md`, section *Recommended: go_router with go_router_builder*: +Two hard rules from `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md`, section *Recommended: go_router with go_router_builder*: - No string-based paths. Navigate with generated route classes, `ProductDetailRoute(id: id).go(context)`, never `context.push('/product/123')`. A string path fails at runtime instead of compile time, and it forces the feature to know the app's URL structure. - Features never import the app router configuration or another feature's route classes. @@ -53,7 +53,7 @@ apps/my_app/lib/app_router/ routes.g.dart generated part ``` -`routes.dart` holds every import and every `part` directive; each feature file starts with `part of 'routes.dart';`. Adding a feature is two lines in the shared file plus a new file nobody else is editing. Read `references/ffca/navigation.md`, section *Splitting the routing table across files*. +`routes.dart` holds every import and every `part` directive; each feature file starts with `part of 'routes.dart';`. Adding a feature is two lines in the shared file plus a new file nobody else is editing. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md`, section *Splitting the routing table across files*. ## Deferred imports live in the routing table @@ -64,16 +64,16 @@ import 'package:favorites_presentation/favorites_list.dart' deferred as favorites_list; ``` -Before relying on this, read `references/ffca/project_structure.md`, section *Deferred loading*. A deferred package must not also be reachable from the app through a non-deferred path, or the chunk never splits, silently. This is informational on an iOS/Android-only app, where the AOT snapshot contains the whole program regardless. +Before relying on this, read `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Deferred loading*. A deferred package must not also be reachable from the app through a non-deferred path, or the chunk never splits, silently. This is informational on an iOS/Android-only app, where the AOT snapshot contains the whole program regardless. ## Deep links and the $extra pattern -Every screen must be rebuildable from its URL parameters alone. `$extra` is a volatile optimization that is lost on browser refresh, process death, and cold-start deep links, so the route declares it nullable and the Bloc treats it as optional: emit `Loaded` immediately when it is present, otherwise fetch by id and show loading. Read `references/ffca/navigation.md`, section *Deep links and data hydration*, and adapt the `ProductDetailRoute` template. +Every screen must be rebuildable from its URL parameters alone. `$extra` is a volatile optimization that is lost on browser refresh, process death, and cold-start deep links, so the route declares it nullable and the Bloc treats it as optional: emit `Loaded` immediately when it is present, otherwise fetch by id and show loading. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md`, section *Deep links and data hydration*, and adapt the `ProductDetailRoute` template. ## Visual extension points -The same inversion applies to widgets. A feature can declare a slot for a widget it does not own and let the app fill it, exactly as it declares a callback for a destination it does not own. Read `references/ffca/navigation.md`, section *Visual extension points*, then use the `ffca-cross-feature` skill to choose between a slot and a direct import. +The same inversion applies to widgets. A feature can declare a slot for a widget it does not own and let the app fill it, exactly as it declares a callback for a destination it does not own. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/navigation.md`, section *Visual extension points*, then use the `ffca-cross-feature` skill to choose between a slot and a direct import. ## Add-to-app -A module is a self-contained entry point with explicit dependencies, so it can be instantiated directly as the root widget of a `FlutterEngine` instead of inside a `GoRouteData.build()`. The module code is identical; only the callback targets change, from go_router routes to platform channels. Read `references/ffca/project_structure.md`, section *Add-to-app support*. +A module is a self-contained entry point with explicit dependencies, so it can be instantiated directly as the root widget of a `FlutterEngine` instead of inside a `GoRouteData.build()`. The module code is identical; only the callback targets change, from go_router routes to platform channels. Read `${CLAUDE_PLUGIN_ROOT}/references/ffca/project_structure.md`, section *Add-to-app support*.