claude-code-pilot 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +11 -11
  2. package/bin/install.js +19 -1
  3. package/manifest.json +5 -1
  4. package/package.json +2 -2
  5. package/src/agents/a11y-architect.md +141 -0
  6. package/src/agents/code-architect.md +71 -0
  7. package/src/agents/code-explorer.md +69 -0
  8. package/src/agents/code-simplifier.md +47 -0
  9. package/src/agents/comment-analyzer.md +45 -0
  10. package/src/agents/csharp-reviewer.md +101 -0
  11. package/src/agents/dart-build-resolver.md +201 -0
  12. package/src/agents/pr-test-analyzer.md +45 -0
  13. package/src/agents/silent-failure-hunter.md +50 -0
  14. package/src/agents/type-design-analyzer.md +41 -0
  15. package/src/available-rules/README.md +3 -1
  16. package/src/available-rules/dart/coding-style.md +159 -0
  17. package/src/available-rules/dart/hooks.md +66 -0
  18. package/src/available-rules/dart/patterns.md +261 -0
  19. package/src/available-rules/dart/security.md +135 -0
  20. package/src/available-rules/dart/testing.md +215 -0
  21. package/src/available-rules/web/coding-style.md +105 -0
  22. package/src/available-rules/web/design-quality.md +72 -0
  23. package/src/available-rules/web/hooks.md +129 -0
  24. package/src/available-rules/web/patterns.md +88 -0
  25. package/src/available-rules/web/performance.md +73 -0
  26. package/src/available-rules/web/security.md +66 -0
  27. package/src/available-rules/web/testing.md +64 -0
  28. package/src/commands/ccp/ai-integration-phase.md +36 -0
  29. package/src/commands/ccp/audit-fix.md +33 -0
  30. package/src/commands/ccp/code-review-fix.md +52 -0
  31. package/src/commands/ccp/eval-review.md +32 -0
  32. package/src/commands/ccp/extract_learnings.md +22 -0
  33. package/src/commands/ccp/import.md +37 -0
  34. package/src/commands/ccp/ingest-docs.md +42 -0
  35. package/src/commands/ccp/intel.md +179 -0
  36. package/src/commands/ccp/plan-review-convergence.md +58 -0
  37. package/src/commands/ccp/scan.md +26 -0
  38. package/src/commands/ccp/sketch-wrap-up.md +31 -0
  39. package/src/commands/ccp/sketch.md +54 -0
  40. package/src/commands/ccp/spec-phase.md +62 -0
  41. package/src/commands/ccp/spike-wrap-up.md +31 -0
  42. package/src/commands/ccp/spike.md +51 -0
  43. package/src/commands/ccp/ultraplan-phase.md +33 -0
  44. package/src/hooks/ccp-read-injection-scanner.js +152 -0
  45. package/src/hooks/kit-check-update.js +59 -7
  46. package/src/hooks/run-with-flags-shell.sh +1 -0
  47. package/src/hooks/run-with-flags.js +48 -1
  48. package/src/hooks/session-end.js +88 -1
  49. package/src/lib/hook-flags.js +14 -0
  50. package/src/pilot/references/agent-contracts.md +79 -0
  51. package/src/pilot/references/ai-evals.md +156 -0
  52. package/src/pilot/references/ai-frameworks.md +186 -0
  53. package/src/pilot/references/doc-conflict-engine.md +91 -0
  54. package/src/pilot/references/gate-prompts.md +100 -0
  55. package/src/pilot/references/gates.md +70 -0
  56. package/src/pilot/references/mandatory-initial-read.md +2 -0
  57. package/src/pilot/references/project-skills-discovery.md +19 -0
  58. package/src/pilot/references/revision-loop.md +97 -0
  59. package/src/pilot/references/sketch-interactivity.md +41 -0
  60. package/src/pilot/references/sketch-theme-system.md +94 -0
  61. package/src/pilot/references/sketch-tooling.md +45 -0
  62. package/src/pilot/references/sketch-variant-patterns.md +81 -0
  63. package/src/pilot/references/thinking-models-debug.md +44 -0
  64. package/src/pilot/references/thinking-models-execution.md +50 -0
  65. package/src/pilot/references/thinking-models-planning.md +62 -0
  66. package/src/pilot/references/thinking-models-research.md +50 -0
  67. package/src/pilot/references/thinking-models-verification.md +55 -0
  68. package/src/pilot/templates/AI-SPEC.md +246 -0
  69. package/src/pilot/templates/spec.md +307 -0
  70. package/src/pilot/workflows/ai-integration-phase.md +284 -0
  71. package/src/pilot/workflows/audit-fix.md +175 -0
  72. package/src/pilot/workflows/code-review-fix.md +497 -0
  73. package/src/pilot/workflows/eval-review.md +155 -0
  74. package/src/pilot/workflows/extract_learnings.md +242 -0
  75. package/src/pilot/workflows/import.md +246 -0
  76. package/src/pilot/workflows/ingest-docs.md +328 -0
  77. package/src/pilot/workflows/plan-review-convergence.md +329 -0
  78. package/src/pilot/workflows/scan.md +102 -0
  79. package/src/pilot/workflows/sketch-wrap-up.md +285 -0
  80. package/src/pilot/workflows/sketch.md +360 -0
  81. package/src/pilot/workflows/spec-phase.md +262 -0
  82. package/src/pilot/workflows/spike-wrap-up.md +306 -0
  83. package/src/pilot/workflows/spike.md +452 -0
  84. package/src/pilot/workflows/ultraplan-phase.md +189 -0
  85. package/src/skills/accessibility/SKILL.md +146 -0
  86. package/src/skills/agent-eval/SKILL.md +145 -0
  87. package/src/skills/agent-introspection-debugging/SKILL.md +153 -0
  88. package/src/skills/android-clean-architecture/SKILL.md +339 -0
  89. package/src/skills/api-connector-builder/SKILL.md +120 -0
  90. package/src/skills/code-tour/SKILL.md +236 -0
  91. package/src/skills/compose-multiplatform-patterns/SKILL.md +299 -0
  92. package/src/skills/csharp-testing/SKILL.md +321 -0
  93. package/src/skills/dart-flutter-patterns/SKILL.md +563 -0
  94. package/src/skills/dashboard-builder/SKILL.md +108 -0
  95. package/src/skills/dotnet-patterns/SKILL.md +321 -0
  96. package/src/skills/frontend-design/SKILL.md +145 -0
  97. package/src/skills/frontend-slides/SKILL.md +184 -0
  98. package/src/skills/frontend-slides/STYLE_PRESETS.md +330 -0
  99. package/src/skills/gateguard/SKILL.md +121 -0
  100. package/src/skills/github-ops/SKILL.md +144 -0
  101. package/src/skills/hookify-rules/SKILL.md +128 -0
  102. package/src/skills/knowledge-ops/SKILL.md +154 -0
  103. package/src/skills/liquid-glass-design/SKILL.md +279 -0
  104. package/src/skills/nestjs-patterns/SKILL.md +230 -0
  105. package/src/skills/security-bounty-hunter/SKILL.md +99 -0
  106. package/src/skills/swift-actor-persistence/SKILL.md +143 -0
  107. package/src/skills/swift-protocol-di-testing/SKILL.md +190 -0
  108. package/src/skills/swiftui-patterns/SKILL.md +259 -0
  109. package/src/skills/terminal-ops/SKILL.md +109 -0
  110. package/src/skills/ui-demo/SKILL.md +465 -0
@@ -0,0 +1,261 @@
1
+ ---
2
+ paths:
3
+ - "**/*.dart"
4
+ - "**/pubspec.yaml"
5
+ ---
6
+ # Dart/Flutter Patterns
7
+
8
+ > This file extends [common/patterns.md](../common/patterns.md) with Dart, Flutter, and common ecosystem-specific content.
9
+
10
+ ## Repository Pattern
11
+
12
+ ```dart
13
+ abstract interface class UserRepository {
14
+ Future<User?> getById(String id);
15
+ Future<List<User>> getAll();
16
+ Stream<List<User>> watchAll();
17
+ Future<void> save(User user);
18
+ Future<void> delete(String id);
19
+ }
20
+
21
+ class UserRepositoryImpl implements UserRepository {
22
+ const UserRepositoryImpl(this._remote, this._local);
23
+
24
+ final UserRemoteDataSource _remote;
25
+ final UserLocalDataSource _local;
26
+
27
+ @override
28
+ Future<User?> getById(String id) async {
29
+ final local = await _local.getById(id);
30
+ if (local != null) return local;
31
+ final remote = await _remote.getById(id);
32
+ if (remote != null) await _local.save(remote);
33
+ return remote;
34
+ }
35
+
36
+ @override
37
+ Future<List<User>> getAll() async {
38
+ final remote = await _remote.getAll();
39
+ for (final user in remote) {
40
+ await _local.save(user);
41
+ }
42
+ return remote;
43
+ }
44
+
45
+ @override
46
+ Stream<List<User>> watchAll() => _local.watchAll();
47
+
48
+ @override
49
+ Future<void> save(User user) => _local.save(user);
50
+
51
+ @override
52
+ Future<void> delete(String id) async {
53
+ await _remote.delete(id);
54
+ await _local.delete(id);
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## State Management: BLoC/Cubit
60
+
61
+ ```dart
62
+ // Cubit — simple state transitions
63
+ class CounterCubit extends Cubit<int> {
64
+ CounterCubit() : super(0);
65
+
66
+ void increment() => emit(state + 1);
67
+ void decrement() => emit(state - 1);
68
+ }
69
+
70
+ // BLoC — event-driven
71
+ @immutable
72
+ sealed class CartEvent {}
73
+ class CartItemAdded extends CartEvent { CartItemAdded(this.item); final Item item; }
74
+ class CartItemRemoved extends CartEvent { CartItemRemoved(this.id); final String id; }
75
+ class CartCleared extends CartEvent {}
76
+
77
+ @immutable
78
+ class CartState {
79
+ const CartState({this.items = const []});
80
+ final List<Item> items;
81
+ CartState copyWith({List<Item>? items}) => CartState(items: items ?? this.items);
82
+ }
83
+
84
+ class CartBloc extends Bloc<CartEvent, CartState> {
85
+ CartBloc() : super(const CartState()) {
86
+ on<CartItemAdded>((event, emit) =>
87
+ emit(state.copyWith(items: [...state.items, event.item])));
88
+ on<CartItemRemoved>((event, emit) =>
89
+ emit(state.copyWith(items: state.items.where((i) => i.id != event.id).toList())));
90
+ on<CartCleared>((_, emit) => emit(const CartState()));
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## State Management: Riverpod
96
+
97
+ ```dart
98
+ // Simple provider
99
+ @riverpod
100
+ Future<List<User>> users(Ref ref) async {
101
+ final repo = ref.watch(userRepositoryProvider);
102
+ return repo.getAll();
103
+ }
104
+
105
+ // Notifier for mutable state
106
+ @riverpod
107
+ class CartNotifier extends _$CartNotifier {
108
+ @override
109
+ List<Item> build() => [];
110
+
111
+ void add(Item item) => state = [...state, item];
112
+ void remove(String id) => state = state.where((i) => i.id != id).toList();
113
+ void clear() => state = [];
114
+ }
115
+
116
+ // ConsumerWidget
117
+ class CartPage extends ConsumerWidget {
118
+ const CartPage({super.key});
119
+
120
+ @override
121
+ Widget build(BuildContext context, WidgetRef ref) {
122
+ final items = ref.watch(cartNotifierProvider);
123
+ return ListView(
124
+ children: items.map((item) => CartItemTile(item: item)).toList(),
125
+ );
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## Dependency Injection
131
+
132
+ Constructor injection is preferred. Use `get_it` or Riverpod providers at composition root:
133
+
134
+ ```dart
135
+ // get_it registration (in a setup file)
136
+ void setupDependencies() {
137
+ final di = GetIt.instance;
138
+ di.registerSingleton<ApiClient>(ApiClient(baseUrl: Env.apiUrl));
139
+ di.registerSingleton<UserRepository>(
140
+ UserRepositoryImpl(di<ApiClient>(), di<LocalDatabase>()),
141
+ );
142
+ di.registerFactory(() => UserListViewModel(di<UserRepository>()));
143
+ }
144
+ ```
145
+
146
+ ## ViewModel Pattern (without BLoC/Riverpod)
147
+
148
+ ```dart
149
+ class UserListViewModel extends ChangeNotifier {
150
+ UserListViewModel(this._repository);
151
+
152
+ final UserRepository _repository;
153
+
154
+ AsyncState<List<User>> _state = const Loading();
155
+ AsyncState<List<User>> get state => _state;
156
+
157
+ Future<void> load() async {
158
+ _state = const Loading();
159
+ notifyListeners();
160
+ try {
161
+ final users = await _repository.getAll();
162
+ _state = Success(users);
163
+ } on Exception catch (e) {
164
+ _state = Failure(e);
165
+ }
166
+ notifyListeners();
167
+ }
168
+ }
169
+ ```
170
+
171
+ ## UseCase Pattern
172
+
173
+ ```dart
174
+ class GetUserUseCase {
175
+ const GetUserUseCase(this._repository);
176
+ final UserRepository _repository;
177
+
178
+ Future<User?> call(String id) => _repository.getById(id);
179
+ }
180
+
181
+ class CreateUserUseCase {
182
+ const CreateUserUseCase(this._repository, this._idGenerator);
183
+ final UserRepository _repository;
184
+ final IdGenerator _idGenerator; // injected — domain layer must not depend on uuid package directly
185
+
186
+ Future<void> call(CreateUserInput input) async {
187
+ // Validate, apply business rules, then persist
188
+ final user = User(id: _idGenerator.generate(), name: input.name, email: input.email);
189
+ await _repository.save(user);
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## Immutable State with freezed
195
+
196
+ ```dart
197
+ @freezed
198
+ class UserState with _$UserState {
199
+ const factory UserState({
200
+ @Default([]) List<User> users,
201
+ @Default(false) bool isLoading,
202
+ String? errorMessage,
203
+ }) = _UserState;
204
+ }
205
+ ```
206
+
207
+ ## Clean Architecture Layer Boundaries
208
+
209
+ ```
210
+ lib/
211
+ ├── domain/ # Pure Dart — no Flutter, no external packages
212
+ │ ├── entities/
213
+ │ ├── repositories/ # Abstract interfaces
214
+ │ └── usecases/
215
+ ├── data/ # Implements domain interfaces
216
+ │ ├── datasources/
217
+ │ ├── models/ # DTOs with fromJson/toJson
218
+ │ └── repositories/
219
+ └── presentation/ # Flutter widgets + state management
220
+ ├── pages/
221
+ ├── widgets/
222
+ └── providers/ (or blocs/ or viewmodels/)
223
+ ```
224
+
225
+ - Domain must not import `package:flutter` or any data-layer package
226
+ - Data layer maps DTOs to domain entities at repository boundaries
227
+ - Presentation calls use cases, not repositories directly
228
+
229
+ ## Navigation (GoRouter)
230
+
231
+ ```dart
232
+ final router = GoRouter(
233
+ routes: [
234
+ GoRoute(
235
+ path: '/',
236
+ builder: (context, state) => const HomePage(),
237
+ ),
238
+ GoRoute(
239
+ path: '/users/:id',
240
+ builder: (context, state) {
241
+ final id = state.pathParameters['id']!;
242
+ return UserDetailPage(userId: id);
243
+ },
244
+ ),
245
+ ],
246
+ // refreshListenable re-evaluates redirect whenever auth state changes
247
+ refreshListenable: GoRouterRefreshStream(authCubit.stream),
248
+ redirect: (context, state) {
249
+ final isLoggedIn = context.read<AuthCubit>().state is AuthAuthenticated;
250
+ if (!isLoggedIn && !state.matchedLocation.startsWith('/login')) {
251
+ return '/login';
252
+ }
253
+ return null;
254
+ },
255
+ );
256
+ ```
257
+
258
+ ## References
259
+
260
+ See skill: `flutter-dart-code-review` for the comprehensive review checklist.
261
+ See skill: `compose-multiplatform-patterns` for Kotlin Multiplatform/Flutter interop patterns.
@@ -0,0 +1,135 @@
1
+ ---
2
+ paths:
3
+ - "**/*.dart"
4
+ - "**/pubspec.yaml"
5
+ - "**/AndroidManifest.xml"
6
+ - "**/Info.plist"
7
+ ---
8
+ # Dart/Flutter Security
9
+
10
+ > This file extends [common/security.md](../common/security.md) with Dart, Flutter, and mobile-specific content.
11
+
12
+ ## Secrets Management
13
+
14
+ - Never hardcode API keys, tokens, or credentials in Dart source
15
+ - Use `--dart-define` or `--dart-define-from-file` for compile-time config (values are not truly secret — use a backend proxy for server-side secrets)
16
+ - Use `flutter_dotenv` or equivalent, with `.env` files listed in `.gitignore`
17
+ - Store runtime secrets in platform-secure storage: `flutter_secure_storage` (Keychain on iOS, EncryptedSharedPreferences on Android)
18
+
19
+ ```dart
20
+ // BAD
21
+ const apiKey = 'sk-abc123...';
22
+
23
+ // GOOD — compile-time config (not secret, just configurable)
24
+ const apiKey = String.fromEnvironment('API_KEY');
25
+
26
+ // GOOD — runtime secret from secure storage
27
+ final token = await secureStorage.read(key: 'auth_token');
28
+ ```
29
+
30
+ ## Network Security
31
+
32
+ - Enforce HTTPS — no `http://` calls in production
33
+ - Configure Android `network_security_config.xml` to block cleartext traffic
34
+ - Set `NSAppTransportSecurity` in `Info.plist` to disallow arbitrary loads
35
+ - Set request timeouts on all HTTP clients — never leave defaults
36
+ - Consider certificate pinning for high-security endpoints
37
+
38
+ ```dart
39
+ // Dio with timeout and HTTPS enforcement
40
+ final dio = Dio(BaseOptions(
41
+ baseUrl: 'https://api.example.com',
42
+ connectTimeout: const Duration(seconds: 10),
43
+ receiveTimeout: const Duration(seconds: 30),
44
+ ));
45
+ ```
46
+
47
+ ## Input Validation
48
+
49
+ - Validate and sanitize all user input before sending to API or storage
50
+ - Never pass unsanitized input to SQL queries — use parameterized queries (sqflite, drift)
51
+ - Sanitize deep link URLs before navigation — validate scheme, host, and path parameters
52
+ - Use `Uri.tryParse` and validate before navigating
53
+
54
+ ```dart
55
+ // BAD — SQL injection
56
+ await db.rawQuery("SELECT * FROM users WHERE email = '$userInput'");
57
+
58
+ // GOOD — parameterized
59
+ await db.query('users', where: 'email = ?', whereArgs: [userInput]);
60
+
61
+ // BAD — unvalidated deep link
62
+ final uri = Uri.parse(incomingLink);
63
+ context.go(uri.path); // could navigate to any route
64
+
65
+ // GOOD — validated deep link
66
+ final uri = Uri.tryParse(incomingLink);
67
+ if (uri != null && uri.host == 'myapp.com' && _allowedPaths.contains(uri.path)) {
68
+ context.go(uri.path);
69
+ }
70
+ ```
71
+
72
+ ## Data Protection
73
+
74
+ - Store tokens, PII, and credentials only in `flutter_secure_storage`
75
+ - Never write sensitive data to `SharedPreferences` or local files in plaintext
76
+ - Clear auth state on logout: tokens, cached user data, cookies
77
+ - Use biometric authentication (`local_auth`) for sensitive operations
78
+ - Avoid logging sensitive data — no `print(token)` or `debugPrint(password)`
79
+
80
+ ## Android-Specific
81
+
82
+ - Declare only required permissions in `AndroidManifest.xml`
83
+ - Export Android components (`Activity`, `Service`, `BroadcastReceiver`) only when necessary; add `android:exported="false"` where not needed
84
+ - Review intent filters — exported components with implicit intent filters are accessible by any app
85
+ - Use `FLAG_SECURE` for screens displaying sensitive data (prevents screenshots)
86
+
87
+ ```xml
88
+ <!-- AndroidManifest.xml — restrict exported components -->
89
+ <activity android:name=".MainActivity" android:exported="true">
90
+ <!-- Only the launcher activity needs exported=true -->
91
+ </activity>
92
+ <activity android:name=".SensitiveActivity" android:exported="false" />
93
+ ```
94
+
95
+ ## iOS-Specific
96
+
97
+ - Declare only required usage descriptions in `Info.plist` (`NSCameraUsageDescription`, etc.)
98
+ - Store secrets in Keychain — `flutter_secure_storage` uses Keychain on iOS
99
+ - Use App Transport Security (ATS) — disallow arbitrary loads
100
+ - Enable data protection entitlement for sensitive files
101
+
102
+ ## WebView Security
103
+
104
+ - Use `webview_flutter` v4+ (`WebViewController` / `WebViewWidget`) — the legacy `WebView` widget is removed
105
+ - Disable JavaScript unless explicitly required (`JavaScriptMode.disabled`)
106
+ - Validate URLs before loading — never load arbitrary URLs from deep links
107
+ - Never expose Dart callbacks to JavaScript unless absolutely needed and carefully sandboxed
108
+ - Use `NavigationDelegate.onNavigationRequest` to intercept and validate navigation requests
109
+
110
+ ```dart
111
+ // webview_flutter v4+ API (WebViewController + WebViewWidget)
112
+ final controller = WebViewController()
113
+ ..setJavaScriptMode(JavaScriptMode.disabled) // disabled unless required
114
+ ..setNavigationDelegate(
115
+ NavigationDelegate(
116
+ onNavigationRequest: (request) {
117
+ final uri = Uri.tryParse(request.url);
118
+ if (uri == null || uri.host != 'trusted.example.com') {
119
+ return NavigationDecision.prevent;
120
+ }
121
+ return NavigationDecision.navigate;
122
+ },
123
+ ),
124
+ );
125
+
126
+ // In your widget tree:
127
+ WebViewWidget(controller: controller)
128
+ ```
129
+
130
+ ## Obfuscation and Build Security
131
+
132
+ - Enable obfuscation in release builds: `flutter build apk --obfuscate --split-debug-info=./debug-info/`
133
+ - Keep `--split-debug-info` output out of version control (used for crash symbolication only)
134
+ - Ensure ProGuard/R8 rules don't inadvertently expose serialized classes
135
+ - Run `flutter analyze` and address all warnings before release
@@ -0,0 +1,215 @@
1
+ ---
2
+ paths:
3
+ - "**/*.dart"
4
+ - "**/pubspec.yaml"
5
+ - "**/analysis_options.yaml"
6
+ ---
7
+ # Dart/Flutter Testing
8
+
9
+ > This file extends [common/testing.md](../common/testing.md) with Dart and Flutter-specific content.
10
+
11
+ ## Test Framework
12
+
13
+ - **flutter_test** / **dart:test** — built-in test runner
14
+ - **mockito** (with `@GenerateMocks`) or **mocktail** (no codegen) for mocking
15
+ - **bloc_test** for BLoC/Cubit unit tests
16
+ - **fake_async** for controlling time in unit tests
17
+ - **integration_test** for end-to-end device tests
18
+
19
+ ## Test Types
20
+
21
+ | Type | Tool | Location | When to Write |
22
+ |------|------|----------|---------------|
23
+ | Unit | `dart:test` | `test/unit/` | All domain logic, state managers, repositories |
24
+ | Widget | `flutter_test` | `test/widget/` | All widgets with meaningful behavior |
25
+ | Golden | `flutter_test` | `test/golden/` | Design-critical UI components |
26
+ | Integration | `integration_test` | `integration_test/` | Critical user flows on real device/emulator |
27
+
28
+ ## Unit Tests: State Managers
29
+
30
+ ### BLoC with `bloc_test`
31
+
32
+ ```dart
33
+ group('CartBloc', () {
34
+ late CartBloc bloc;
35
+ late MockCartRepository repository;
36
+
37
+ setUp(() {
38
+ repository = MockCartRepository();
39
+ bloc = CartBloc(repository);
40
+ });
41
+
42
+ tearDown(() => bloc.close());
43
+
44
+ blocTest<CartBloc, CartState>(
45
+ 'emits updated items when CartItemAdded',
46
+ build: () => bloc,
47
+ act: (b) => b.add(CartItemAdded(testItem)),
48
+ expect: () => [CartState(items: [testItem])],
49
+ );
50
+
51
+ blocTest<CartBloc, CartState>(
52
+ 'emits empty cart when CartCleared',
53
+ seed: () => CartState(items: [testItem]),
54
+ build: () => bloc,
55
+ act: (b) => b.add(CartCleared()),
56
+ expect: () => [const CartState()],
57
+ );
58
+ });
59
+ ```
60
+
61
+ ### Riverpod with `ProviderContainer`
62
+
63
+ ```dart
64
+ test('usersProvider loads users from repository', () async {
65
+ final container = ProviderContainer(
66
+ overrides: [userRepositoryProvider.overrideWithValue(FakeUserRepository())],
67
+ );
68
+ addTearDown(container.dispose);
69
+
70
+ final result = await container.read(usersProvider.future);
71
+ expect(result, isNotEmpty);
72
+ });
73
+ ```
74
+
75
+ ## Widget Tests
76
+
77
+ ```dart
78
+ testWidgets('CartPage shows item count badge', (tester) async {
79
+ await tester.pumpWidget(
80
+ ProviderScope(
81
+ overrides: [
82
+ cartNotifierProvider.overrideWith(() => FakeCartNotifier([testItem])),
83
+ ],
84
+ child: const MaterialApp(home: CartPage()),
85
+ ),
86
+ );
87
+
88
+ await tester.pump();
89
+ expect(find.text('1'), findsOneWidget);
90
+ expect(find.byType(CartItemTile), findsOneWidget);
91
+ });
92
+
93
+ testWidgets('shows empty state when cart is empty', (tester) async {
94
+ await tester.pumpWidget(
95
+ ProviderScope(
96
+ overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier([]))],
97
+ child: const MaterialApp(home: CartPage()),
98
+ ),
99
+ );
100
+
101
+ await tester.pump();
102
+ expect(find.text('Your cart is empty'), findsOneWidget);
103
+ });
104
+ ```
105
+
106
+ ## Fakes Over Mocks
107
+
108
+ Prefer hand-written fakes for complex dependencies:
109
+
110
+ ```dart
111
+ class FakeUserRepository implements UserRepository {
112
+ final _users = <String, User>{};
113
+ Object? fetchError;
114
+
115
+ @override
116
+ Future<User?> getById(String id) async {
117
+ if (fetchError != null) throw fetchError!;
118
+ return _users[id];
119
+ }
120
+
121
+ @override
122
+ Future<List<User>> getAll() async {
123
+ if (fetchError != null) throw fetchError!;
124
+ return _users.values.toList();
125
+ }
126
+
127
+ @override
128
+ Stream<List<User>> watchAll() => Stream.value(_users.values.toList());
129
+
130
+ @override
131
+ Future<void> save(User user) async {
132
+ _users[user.id] = user;
133
+ }
134
+
135
+ @override
136
+ Future<void> delete(String id) async {
137
+ _users.remove(id);
138
+ }
139
+
140
+ void addUser(User user) => _users[user.id] = user;
141
+ }
142
+ ```
143
+
144
+ ## Async Testing
145
+
146
+ ```dart
147
+ // Use fake_async for controlling timers and Futures
148
+ test('debounce triggers after 300ms', () {
149
+ fakeAsync((async) {
150
+ final debouncer = Debouncer(delay: const Duration(milliseconds: 300));
151
+ var callCount = 0;
152
+ debouncer.run(() => callCount++);
153
+ expect(callCount, 0);
154
+ async.elapse(const Duration(milliseconds: 200));
155
+ expect(callCount, 0);
156
+ async.elapse(const Duration(milliseconds: 200));
157
+ expect(callCount, 1);
158
+ });
159
+ });
160
+ ```
161
+
162
+ ## Golden Tests
163
+
164
+ ```dart
165
+ testWidgets('UserCard golden test', (tester) async {
166
+ await tester.pumpWidget(
167
+ MaterialApp(home: UserCard(user: testUser)),
168
+ );
169
+
170
+ await expectLater(
171
+ find.byType(UserCard),
172
+ matchesGoldenFile('goldens/user_card.png'),
173
+ );
174
+ });
175
+ ```
176
+
177
+ Run `flutter test --update-goldens` when intentional visual changes are made.
178
+
179
+ ## Test Naming
180
+
181
+ Use descriptive, behavior-focused names:
182
+
183
+ ```dart
184
+ test('returns null when user does not exist', () { ... });
185
+ test('throws NotFoundException when id is empty string', () { ... });
186
+ testWidgets('disables submit button while form is invalid', (tester) async { ... });
187
+ ```
188
+
189
+ ## Test Organization
190
+
191
+ ```
192
+ test/
193
+ ├── unit/
194
+ │ ├── domain/
195
+ │ │ └── usecases/
196
+ │ └── data/
197
+ │ └── repositories/
198
+ ├── widget/
199
+ │ └── presentation/
200
+ │ └── pages/
201
+ └── golden/
202
+ └── widgets/
203
+
204
+ integration_test/
205
+ └── flows/
206
+ ├── login_flow_test.dart
207
+ └── checkout_flow_test.dart
208
+ ```
209
+
210
+ ## Coverage
211
+
212
+ - Target 80%+ line coverage for business logic (domain + state managers)
213
+ - All state transitions must have tests: loading → success, loading → error, retry
214
+ - Run `flutter test --coverage` and inspect `lcov.info` with a coverage reporter
215
+ - Coverage failures should block CI when below threshold
@@ -0,0 +1,105 @@
1
+ ---
2
+ paths:
3
+ - "**/*.html"
4
+ - "**/*.css"
5
+ - "**/*.js"
6
+ - "**/*.jsx"
7
+ - "**/*.ts"
8
+ - "**/*.tsx"
9
+ ---
10
+ > This file extends [common/coding-style.md](../common/coding-style.md) with web-specific frontend content.
11
+
12
+ # Web Coding Style
13
+
14
+ ## File Organization
15
+
16
+ Organize by feature or surface area, not by file type:
17
+
18
+ ```text
19
+ src/
20
+ ├── components/
21
+ │ ├── hero/
22
+ │ │ ├── Hero.tsx
23
+ │ │ ├── HeroVisual.tsx
24
+ │ │ └── hero.css
25
+ │ ├── scrolly-section/
26
+ │ │ ├── ScrollySection.tsx
27
+ │ │ ├── StickyVisual.tsx
28
+ │ │ └── scrolly.css
29
+ │ └── ui/
30
+ │ ├── Button.tsx
31
+ │ ├── SurfaceCard.tsx
32
+ │ └── AnimatedText.tsx
33
+ ├── hooks/
34
+ │ ├── useReducedMotion.ts
35
+ │ └── useScrollProgress.ts
36
+ ├── lib/
37
+ │ ├── animation.ts
38
+ │ └── color.ts
39
+ └── styles/
40
+ ├── tokens.css
41
+ ├── typography.css
42
+ └── global.css
43
+ ```
44
+
45
+ ## CSS Custom Properties
46
+
47
+ Define design tokens as variables. Do not hardcode palette, typography, or spacing repeatedly:
48
+
49
+ ```css
50
+ :root {
51
+ --color-surface: oklch(98% 0 0);
52
+ --color-text: oklch(18% 0 0);
53
+ --color-accent: oklch(68% 0.21 250);
54
+
55
+ --text-base: clamp(1rem, 0.92rem + 0.4vw, 1.125rem);
56
+ --text-hero: clamp(3rem, 1rem + 7vw, 8rem);
57
+
58
+ --space-section: clamp(4rem, 3rem + 5vw, 10rem);
59
+
60
+ --duration-fast: 150ms;
61
+ --duration-normal: 300ms;
62
+ --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
63
+ }
64
+ ```
65
+
66
+ ## Animation-Only Properties
67
+
68
+ Prefer compositor-friendly motion:
69
+ - `transform`
70
+ - `opacity`
71
+ - `clip-path`
72
+ - `filter` (sparingly)
73
+
74
+ Avoid animating layout-bound properties:
75
+ - `width`
76
+ - `height`
77
+ - `top`
78
+ - `left`
79
+ - `margin`
80
+ - `padding`
81
+ - `border`
82
+ - `font-size`
83
+
84
+ ## Semantic HTML First
85
+
86
+ ```html
87
+ <header>
88
+ <nav aria-label="Main navigation">...</nav>
89
+ </header>
90
+ <main>
91
+ <section aria-labelledby="hero-heading">
92
+ <h1 id="hero-heading">...</h1>
93
+ </section>
94
+ </main>
95
+ <footer>...</footer>
96
+ ```
97
+
98
+ Do not reach for generic wrapper `div` stacks when a semantic element exists.
99
+
100
+ ## Naming
101
+
102
+ - Components: PascalCase (`ScrollySection`, `SurfaceCard`)
103
+ - Hooks: `use` prefix (`useReducedMotion`)
104
+ - CSS classes: kebab-case or utility classes
105
+ - Animation timelines: camelCase with intent (`heroRevealTl`)