Skip to content
Open
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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ A PostToolUse hook runs on every `Edit`, `MultiEdit`, or `Write` and ignores any
| **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 |
| **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 is also runnable directly for CI and audits, across the whole workspace:

```bash
Expand Down
2 changes: 1 addition & 1 deletion agents/ffca-layer-auditor.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ 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, and workspace-wide cycles. It prints each violation with its rule and fix and exits 2 if any are found, 0 if the graph is clean. 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, and 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). It prints each violation with its rule and fix and exits 2 if any are found, 0 if the graph is clean. 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.

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Does not depend on alpha_domain: alpha_domain already depends on alpha_data,
# so adding the edge would plant a cycle instead of the domain -> data rule.
name: alpha_data
resolution: workspace
environment:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ resolution: workspace
environment:
sdk: ^3.11.0
dependencies:
beta_domain: any
beta_presentation: any
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Indented by four spaces: the reader must still see both dependencies.
name: gamma_presentation
resolution: workspace
environment:
sdk: ^3.11.0
sdk: ^3.11.0
dependencies:
gamma_domain: any
delta_data: any
gamma_domain: any
delta_data: any
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Data layer that does not depend on its own domain.
name: iota_data
resolution: workspace
environment:
sdk: ^3.11.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: iota_domain
resolution: workspace
environment:
sdk: ^3.11.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Data layer in a feature that has no domain package.
name: kappa_data
resolution: workspace
environment:
sdk: ^3.11.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Domain layer that depends on the Flutter SDK.
name: lambda_domain
resolution: workspace
environment:
sdk: ^3.11.0
dependencies:
flutter:
sdk: flutter
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Data layer that declares its own domain only as a dev dependency.
name: mu_data
resolution: workspace
environment:
sdk: ^3.11.0
dev_dependencies:
mu_domain: any
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: mu_domain
resolution: workspace
environment:
sdk: ^3.11.0
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ resolution: workspace
environment:
sdk: ^3.11.0
dependencies:
theta_domain: any
delta_data:
path: ../../delta/delta_data
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: theta_domain
resolution: workspace
environment:
sdk: ^3.11.0
7 changes: 7 additions & 0 deletions scripts/test/fixtures/invalid_workspace/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ workspace:
- features/eta/eta_domain
- features/gamma/gamma_domain
- features/gamma/gamma_presentation
- features/iota/iota_data
- features/iota/iota_domain
- features/kappa/kappa_data
- features/lambda/lambda_domain
- features/mu/mu_data
- features/mu/mu_domain
- features/orders/orders_service
- features/theta/theta_data
- features/theta/theta_domain
- features/zeta/zeta_domain
- shared/bad_shared
116 changes: 114 additions & 2 deletions scripts/test/validate_layers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,36 @@ void main() {
expect(err, contains('nothing may depend on an app'));
});

test('data does not depend on its own domain', () {
expect(err, contains('iota_data does not depend on iota_domain'));
expect(err, contains("a data layer implements its own feature's domain"));
expect(err, contains('Add iota_domain under dependencies'));
});

test('data declares its own domain only as a dev dependency', () {
expect(
err,
contains('mu_data declares mu_domain only outside dependencies'),
);
});

test('data in a feature with no domain', () {
expect(
err,
contains('kappa_data is a data layer but features/kappa/ has no '
'kappa_domain'),
);
expect(err, contains('Add kappa_domain with the repository interfaces'));
});

test('domain depends on the Flutter SDK', () {
expect(
err,
contains('lambda_domain depends on the Flutter SDK through flutter'),
);
expect(err, contains('the domain layer is pure Dart'));
});

test('resolves dependencies by workspace name and by path', () {
// The fixture is a Dart workspace: alpha_domain declares `alpha_data:
// any` and resolves by name, theta_data keeps a `path:` dependency.
Expand All @@ -156,9 +186,9 @@ void main() {
});

test('every violation includes a fix line', () {
// One arrow per violation; there are at least the seven planted rules.
// One arrow per violation; there are at least the eleven planted rules.
final arrows = '→'.allMatches(err).length;
expect(arrows, greaterThanOrEqualTo(7));
expect(arrows, greaterThanOrEqualTo(11));
});
});

Expand Down Expand Up @@ -198,6 +228,35 @@ void main() {
expect(r.err, contains('shared packages must never depend on a feature'));
});

test('reports a cycle on the same package from every member', () {
// The cycle is attributed to its alphabetically first package, so a
// suppression placed there works whichever package is edited.
for (final pkg in [
'features/epsilon/epsilon_domain',
'features/zeta/zeta_domain',
]) {
final r = _run(['--file', _pubspec(_invalid, pkg)]);
expect(r.code, 2, reason: pkg);
expect(
r.err,
contains('FFCA violation in features/epsilon/epsilon_domain/'
'pubspec.yaml:\n ✗ dependency cycle detected: '
'epsilon_domain → zeta_domain → epsilon_domain'),
reason: pkg,
);
}
});

test('reads dependencies indented by four spaces', () {
// gamma_presentation's pubspec uses four-space indentation.
final r = _run([
'--file',
_pubspec(_invalid, 'features/gamma/gamma_presentation'),
]);
expect(r.code, 2);
expect(r.err, contains('gamma_presentation depends on delta_data'));
});

test('flags presentation depending on a data layer', () {
final r = _run([
'--file',
Expand Down Expand Up @@ -227,6 +286,59 @@ void main() {
expect(r.code, 2);
expect(r.err, contains('nothing may depend on an app'));
});

test('flags data not depending on its own domain', () {
final r = _run([
'--file',
_pubspec(_invalid, 'features/iota/iota_data'),
]);
expect(r.code, 2);
expect(r.err, contains('iota_data does not depend on iota_domain'));
});

test('flags data declaring its own domain only as a dev dependency', () {
final r = _run([
'--file',
_pubspec(_invalid, 'features/mu/mu_data'),
]);
expect(r.code, 2);
expect(r.err, contains('mu_data declares mu_domain only outside'));
});

test('flags data in a feature with no domain', () {
final r = _run([
'--file',
_pubspec(_invalid, 'features/kappa/kappa_data'),
]);
expect(r.code, 2);
expect(r.err, contains('features/kappa/ has no kappa_domain'));
});

test('flags a domain depending on the Flutter SDK', () {
final r = _run([
'--file',
_pubspec(_invalid, 'features/lambda/lambda_domain'),
]);
expect(r.code, 2);
expect(r.err, contains('the domain layer is pure Dart'));
});

test('planted fixtures report only their own rule', () {
// beta and theta now depend on their own domain, so the ownership
// checks add nothing to the layer rule each of them plants.
for (final pkg in [
'features/beta/beta_data',
'features/theta/theta_data',
]) {
final r = _run(['--file', _pubspec(_invalid, pkg)]);
expect(r.code, 2);
expect(
'✗'.allMatches(r.err).length,
1,
reason: '$pkg should report one violation, got: ${r.err}',
);
}
});
});

group('graceful skips (exit 0)', () {
Expand Down
Loading
Loading