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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ references/
domain_templates.md # Model, repository interface, Command and Query shapes
presentation_templates.md # Cubit, Module, route shapes
scripts/
analysis_options.yaml # Excludes the test fixtures from analysis
pubspec.yaml # Dev dependency on package:test for the validator tests
sync_reference.dart # Regenerates references/ffca/ from VGV Engineering, --check for drift
validate_layers.dart # FFCA layer, naming, and cycle validator
validate_layers.dart # FFCA layer, naming, cycle, import, and barrel validator
test/
validate_layers_test.dart # Validator tests
fixtures/
valid_workspace/ # FFCA repo that must pass
invalid_workspace/ # One package per violation the validator must catch
warning_workspace/ # Warnings and suppressed errors only, must exit 0
not_ffca/ # Non-FFCA repo the validator must skip
skills/
ffca-architecture/SKILL.md # Orientation, layer rules, naming
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

`hooks/hooks.json` defines one PostToolUse hook and one PreToolUse hook.

- `Edit|MultiEdit|Write` matcher → `validate_layers.sh`. The matcher is a filter, not a guarantee, so the hook reads `tool_name` from the payload and exits 0 unless it is `Edit`, `MultiEdit`, or `Write`. When the edited file is a `pubspec.yaml` inside an FFCA-shaped repo, meaning a `features/` folder exists above it, the hook runs `dart run scripts/validate_layers.dart --file <pubspec>`. The validator checks the edited package and its direct dependents. Exit 2 means a layer violation. The hook prints the rule and the fix to stderr and exits 2, which blocks Claude until the dependency is fixed.
- `Edit|MultiEdit|Write` matcher → `validate_layers.sh`. The matcher is a filter, not a guarantee, so the hook reads `tool_name` from the payload and exits 0 unless it is `Edit`, `MultiEdit`, or `Write`. When the edited file is a `pubspec.yaml` or a Dart file inside an FFCA-shaped repo, meaning a `features/` folder exists above it, the hook runs `dart run scripts/validate_layers.dart --file <path>`. For a pubspec, the validator checks the edited package and its direct dependents, reporting the edited package's warnings and errors but only the errors of its dependents, and running the import checks and suppression validation for the edited package only, and it does not require a barrel from a package with no `lib/` yet. For a Dart file under a package's `lib/` or `bin/`, it reports only the import and barrel findings located in that file, so a Dart edit never blocks on a violation elsewhere. Exit 2 means at least one error-severity finding. The hook forwards only the validator's report, from the first `FFCA findings in` line, so `dart run`'s own notices never reach Claude. It prints the findings to stderr and exits 2, which blocks Claude until the dependency is fixed. When the validator exits 0 with output, every finding is a warning: the hook returns it as `additionalContext` JSON and exits 0, so Claude sees it without being blocked. A `# ffca-ignore: <rule-id> — <reason>` comment in a pubspec suppresses that rule for that package, and `// ffca-ignore:` in a Dart file suppresses it for that file, except for rules reported on the pubspec, which a Dart file cannot suppress.
- Any other file, or a repo with no `features/` folder, exits 0 silently.
- The hook skips with exit 0 when `jq` or `dart` is missing from `PATH`. The `dart` check runs only for FFCA pubspec edits. A validator failure other than exit 2 is reported to stderr but never blocks.
- The hook skips with exit 0 when `jq` or `dart` is missing from `PATH`. The `dart` check runs only for FFCA pubspec and Dart edits. A validator failure other than exit 2 is reported to stderr but never blocks.

- `mcp__.*very-good-cli__.*` matcher → `check_vgv_cli.sh`. For a Very Good CLI MCP tool, it returns `allow` when `very_good --version` is 1.3.0 or newer and `deny` with an install or upgrade message when the CLI is missing or older. It stands aside with exit 0 for any other tool, when the version cannot be read, or when `jq` is missing. `allowed-tools` grants only last for the invoking turn, so this hook is what keeps the tools approved afterwards.

`hooks/check_vgv_cli_test.sh` covers the PreToolUse hook against a stubbed `very_good` and runs in CI under the **Hook Tests** job.

`scripts/test/validate_layers_test.dart` covers the validator and runs in CI under the **Layer Validator** job. Add a fixture and a case there when changing a rule.
`scripts/test/validate_layers_test.dart` covers the validator and runs in CI under the **Layer Validator** job. Add a fixture and a case there when changing a rule, and add a new rule as a value of the `Rule` enum, with its ID and severity, and to the rules table in the **Hooks** section of `README.md`.

`hooks/validate_layers_test.sh` covers the PostToolUse hook against the same fixtures and runs in CI under the **Hook Tests** job.
55 changes: 51 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,67 @@ Skills activate automatically when Claude detects an FFCA repo or an FFCA-shaped

## Hooks

A PostToolUse hook runs on every `Edit`, `MultiEdit`, or `Write` and ignores any other tool call. When the edited file is a `pubspec.yaml` inside an FFCA-shaped repo, it validates the package's layer dependencies. A PreToolUse hook gates every Very Good CLI MCP tool call.
A PostToolUse hook runs on every `Edit`, `MultiEdit`, or `Write` and ignores any other tool call. When the edited file is a `pubspec.yaml` inside an FFCA-shaped repo, it validates the package's layer dependencies. When it is a Dart file, it validates that file's imports and exports. A PreToolUse hook gates every Very Good CLI MCP tool call.

| Hook | Event | Behavior |
| --- | --- | --- |
| **Validate layers** (`validate_layers.sh`) | PostToolUse (`Edit`/`MultiEdit`/`Write`) | Runs the FFCA validator incrementally on the edited package and its direct dependents. Exits 2 on a violation (blocking: Claude must fix the dependency before continuing), printing the rule and the fix. Passes silently otherwise |
| **Validate layers** (`validate_layers.sh`) | PostToolUse (`Edit`/`MultiEdit`/`Write`) | Runs the FFCA validator incrementally: on the edited package and its direct dependents for a pubspec edit (errors only for dependents), and on the edited file alone for a Dart edit. Exits 2 on an error (blocking: Claude must fix the dependency before continuing), printing the rule and the fix. Passes warnings to Claude as non-blocking context. Passes silently otherwise |
| **Check VGV CLI** (`check_vgv_cli.sh`) | PreToolUse (`mcp__.*very-good-cli__.*`) | Auto-approves Very Good CLI MCP tool calls when the CLI is installed at 1.3.0 or newer, so they work in every run mode. Denies with an install or upgrade message when the CLI is missing or outdated. Stands aside for any other tool, or when the CLI version cannot be read |

The validator checks package naming under `features/`, the layer dependency rules, that nothing depends on an app, and cycles. It also checks that each feature's packages fit together: a data package depends on its own `_domain` under `dependencies`, a data package never exists without a `_domain` in its feature, and a `_domain` package never depends on the Flutter SDK.
The validator checks package naming under `features/`, the layer dependency rules, that nothing depends on an app, and cycles. It also checks that each feature's packages fit together: a data package depends on its own `_domain` under `dependencies`, a data package never exists without a `_domain` in its feature, and a `_domain` package never depends on the Flutter SDK. In `lib/` and `bin/`, it checks that each feature package has a primary barrel `lib/{package}.dart`, that imports of other workspace packages go through a barrel at the root of their `lib/`, that another feature's presentation is imported through a dedicated barrel rather than its primary one, that a data barrel never exports DTOs or mappers, that every declared workspace dependency is imported, and that every imported workspace package is declared. An import of an undeclared package is checked against the same layer rules as a pubspec dependency, since a Dart workspace resolves it anyway. On a pubspec edit, a package with no `lib/` yet is not required to have a barrel, so a new package can be scaffolded pubspec first.

The validator is also runnable directly for CI and audits, across the whole workspace:

```bash
dart run scripts/validate_layers.dart --all
```

### Rules, severities, and suppressions

Every finding carries a rule ID in brackets. An **error** exits 2 and blocks the hook. A **warning** is reported but exits 0.

| Rule ID | Severity | Finding |
| --- | --- | --- |
| `package-name` | error | A package under `features/{f}/` is not named `{f}_domain`, `{f}_data`, `{f}_data_{backend}`, or `{f}_presentation` |
| `depends-on-app` | error | Any package depends on an app |
| `shared-depends-on-feature` | error | A shared package depends on a feature package |
| `presentation-depends-on-data` | error | A presentation package depends on a data package |
| `domain-depends-on-data` | error | A domain package depends on a data package |
| `domain-depends-on-presentation` | error | A domain package depends on a presentation package |
| `data-depends-on-presentation` | error | A data package depends on a presentation package |
| `data-depends-on-feature-data` | error | A data package depends on another feature's data package |
| `layer-dependency` | error | Any other dependency the rules table does not allow |
| `dependency-cycle` | error | Packages depend on each other in a cycle |
| `data-missing-own-domain` | error | A data package does not depend on its own `_domain` under `dependencies` |
| `data-without-domain` | error | A data package's feature has no `_domain` package |
| `domain-uses-flutter` | error | A domain package depends on the Flutter SDK |
| `presentation-missing-own-domain` | warning | A presentation package does not depend on its feature's existing `_domain` |
| `missing-primary-barrel` | error | A feature package has no `lib/{package}.dart` |
| `deep-import` | error | A Dart file imports or exports another workspace package past the barrels at the root of its `lib/` |
| `cross-feature-primary-barrel` | error | A feature imports another feature's primary presentation barrel instead of a dedicated one |
| `data-barrel-exports-internals` | error | A data package's primary barrel exports from `dtos/` or `mappers/`. Data sources may be exported, since the app constructs them |
| `undeclared-dependency` | error | A Dart file imports a workspace package its pubspec does not declare under `dependencies`. When the layer rules forbid that dependency, the layer rule's ID is reported instead |
| `unused-dependency` | warning | A package declares a workspace dependency under `dependencies` but never imports it in `lib/` or `bin/` |
| `suppression-missing-reason` | error | An `ffca-ignore` comment has no reason |
| `suppression-unknown-rule` | error | An `ffca-ignore` comment names a rule that does not exist or cannot be suppressed, or a `//` comment in a Dart file names a rule reported on the pubspec |
| `unused-suppression` | warning | An `ffca-ignore` comment matches no finding (`--all` only) |

To accept a finding as a deliberate exception, add a comment with the rule ID and a reason to the pubspec of the package it is reported on:

```yaml
# ffca-ignore: data-missing-own-domain — legacy repository, moving to orders_domain in #123
name: orders_data
```

A finding in a Dart file can also be suppressed in that file, with `//`:

```dart
// ffca-ignore: deep-import — the model is not exported from orders_domain yet
import 'package:orders_domain/models/order.dart';
```

The separator after the rule ID can be an em dash, an en dash, a hyphen, or a colon. A suppression in a pubspec covers every finding with that rule ID in that package, and one in a Dart file covers that file. Rules reported on the pubspec, such as `unused-dependency` or `missing-primary-barrel`, can only be suppressed in the pubspec. The reason is required, so each exception explains itself in review. The last three rules cannot be suppressed. `--all` reports how many findings were suppressed, and flags suppressions that no longer match anything so they can be removed.

### Prerequisites

- **Dart SDK** must be available on your `PATH`. The validator imports only `dart:io`, so it runs with just the SDK, no `dart pub get` required.
Expand All @@ -138,6 +184,7 @@ Run the hook tests with:

```bash
bash hooks/check_vgv_cli_test.sh
bash hooks/validate_layers_test.sh
```

## MCP Integration
Expand All @@ -161,7 +208,7 @@ The server needs **Very Good CLI** 1.3.0 or newer. Install it with `dart pub glo

| Agent | Behavior |
| --- | --- |
| [**ffca-layer-auditor**](agents/ffca-layer-auditor.md) | Read-only architecture auditor. Runs the validator in `--all` mode, then adds source-level checks (declared-but-unused dependencies, barrel hygiene, DTO leakage, Command/Query necessity, module entry, split routing tables, deferred-loading reachability, misplaced packages, high fan-in) and returns a per-package verdict table. Reports violations, never auto-fixes |
| [**ffca-layer-auditor**](agents/ffca-layer-auditor.md) | Read-only architecture auditor. Runs the validator in `--all` mode, then adds source-level checks (barrel scope, DTO leakage, Command/Query necessity, module entry, split routing tables, deferred-loading reachability, misplaced packages, high fan-in) and returns a per-package verdict table. Reports violations, never auto-fixes |

The auditor runs in its own context, so the same architecture review can be dispatched from the `ffca-audit` skill, a refactor, or a pre-PR flow without crowding the main conversation. The per-edit hook prevents bad pubspec dependencies as they are written; the agent answers whether the whole repo is healthy on demand.

Expand Down
Loading
Loading