@ds9-solutions/mcp 0.1.0 → 0.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 (68) hide show
  1. package/content/agents/code-reviewer.agent.md +52 -22
  2. package/content/agents/db-analyser.agent.md +121 -0
  3. package/content/agents/security-reviewer.agent.md +119 -37
  4. package/content/agents/test-writer.agent.md +84 -35
  5. package/content/skills/dart/SKILL.md +139 -0
  6. package/content/skills/dart/examples/entity.dart +37 -0
  7. package/content/skills/dart/examples/error_handling.dart +36 -0
  8. package/content/skills/dart/examples/order_test.dart +34 -0
  9. package/content/skills/dart/examples/repository.dart +57 -0
  10. package/content/skills/dart/references/checklist.md +12 -0
  11. package/content/skills/dart/references/documentation.md +26 -0
  12. package/content/skills/dart/references/naming.md +27 -0
  13. package/content/skills/dart-frog/SKILL.md +208 -0
  14. package/content/skills/dart-frog/examples/auth_middleware.dart +50 -0
  15. package/content/skills/dart-frog/examples/controller.dart +75 -0
  16. package/content/skills/dart-frog/examples/dto.dart +41 -0
  17. package/content/skills/dart-frog/examples/error_response.dart +35 -0
  18. package/content/skills/dart-frog/examples/exceptions.dart +60 -0
  19. package/content/skills/dart-frog/examples/middleware_composition.dart +34 -0
  20. package/content/skills/dart-frog/examples/model.dart +20 -0
  21. package/content/skills/dart-frog/examples/port.dart +17 -0
  22. package/content/skills/dart-frog/examples/repository.dart +56 -0
  23. package/content/skills/dart-frog/examples/route_collection.dart +25 -0
  24. package/content/skills/dart-frog/examples/route_item.dart +24 -0
  25. package/content/skills/dart-frog/examples/route_test.dart +33 -0
  26. package/content/skills/dart-frog/examples/service.dart +51 -0
  27. package/content/skills/dart-frog/examples/service_test.dart +55 -0
  28. package/content/skills/dart-frog/references/checklist.md +31 -0
  29. package/content/skills/dart-frog/references/error-handling.md +48 -0
  30. package/content/skills/dart-frog/references/fpdart-taskeither.md +53 -0
  31. package/content/skills/dart-frog/references/layers.md +43 -0
  32. package/content/skills/dart-frog/references/middlewares-di.md +45 -0
  33. package/content/skills/dart-frog/references/project-structure.md +67 -0
  34. package/content/skills/dart-frog/references/routing.md +53 -0
  35. package/content/skills/dart-frog/references/testing.md +24 -0
  36. package/content/skills/docker/SKILL.md +3 -3
  37. package/content/skills/driftorm/SKILL.md +249 -0
  38. package/content/skills/flutter/SKILL.md +419 -0
  39. package/content/skills/hex-pattern-dart/SKILL.md +83 -0
  40. package/content/skills/hex-pattern-dart/examples/adapter-in.md +24 -0
  41. package/content/skills/hex-pattern-dart/examples/adapter-out.md +20 -0
  42. package/content/skills/hex-pattern-dart/examples/errors.md +25 -0
  43. package/content/skills/hex-pattern-dart/examples/port.md +11 -0
  44. package/content/skills/hex-pattern-dart/examples/service.md +20 -0
  45. package/content/skills/hex-pattern-dart/examples/structure-tree.md +28 -0
  46. package/content/skills/hex-pattern-dart/references/anti-patterns.md +16 -0
  47. package/content/skills/hex-pattern-dart/references/checklist.md +34 -0
  48. package/content/skills/hex-pattern-dart/references/dependency-injection.md +32 -0
  49. package/content/skills/hex-pattern-dart/references/error-layers.md +27 -0
  50. package/content/skills/hex-pattern-dart/references/layers-and-dependency-rule.md +29 -0
  51. package/content/skills/hex-pattern-dart/references/persistence.md +23 -0
  52. package/content/skills/hex-pattern-dart/references/ports-and-adapters.md +33 -0
  53. package/content/skills/hex-pattern-dart/references/testing.md +30 -0
  54. package/content/skills/mysql/SKILL.md +3 -3
  55. package/content/skills/typescript/SKILL.md +4 -4
  56. package/dist/src/content.js +10 -2
  57. package/dist/src/init.js +1 -0
  58. package/dist/src/search.js +20 -12
  59. package/dist/src/server.js +60 -4
  60. package/dist/src/skill-files.js +125 -0
  61. package/package.json +2 -2
  62. package/content/agents/db-specialist.agent.md +0 -78
  63. package/content/agents/nestjs-api-designer.agent.md +0 -61
  64. package/content/skills/hexagonal/SKILL.md +0 -156
  65. package/content/skills/jest-testing/SKILL.md +0 -88
  66. package/content/skills/nestjs/SKILL.md +0 -100
  67. package/content/skills/typeorm/SKILL.md +0 -119
  68. package/dist/test/mcp.test.js +0 -112
@@ -0,0 +1,419 @@
1
+ ---
2
+ name: flutter
3
+ description: Padrões de arquitetura, código e qualidade para apps Flutter com Provider, go_router e fpdart. Use ao criar, editar, revisar ou testar arquivos em lib/ ou test/ de um projeto Flutter, como features, providers, services, models, views, rotas, tema ou testes.
4
+ ---
5
+
6
+ # Flutter best practices
7
+
8
+ Arquitetura em camadas com **Provider + ChangeNotifier**, **go_router** e **fpdart** (`TaskEither`/`Either`), alinhada ao guia oficial de arquitetura do Flutter (MVVM com camada de dados) e ao Effective Dart.
9
+
10
+ ## Stack
11
+
12
+ - **Estado:** `provider` com `ChangeNotifier`. Não misture Riverpod, Bloc ou GetX.
13
+ - **Navegação:** `go_router`.
14
+ - **Erros e async:** `fpdart`. Operações que podem falhar retornam `TaskEither`, nunca `Future` que lança exceção entre camadas.
15
+ - **Configuração:** `flutter_dotenv` (`.env`) ou `--dart-define`. Nunca coloque segredos ou URLs fixas no código.
16
+ - **Lints:** `flutter_lints`.
17
+
18
+ ## Estrutura de pastas
19
+
20
+ ```
21
+ lib/
22
+ ├── main.dart # bootstrap + composição de dependências
23
+ ├── components/ # features
24
+ │ └── <feature>/
25
+ │ ├── models/
26
+ │ ├── providers/ # ChangeNotifier (ViewModel)
27
+ │ ├── services/ # acesso a dados via ports (pode ter dtos/ e mappers/)
28
+ │ ├── views/ # telas
29
+ │ │ └── components/ # widgets exclusivos da feature
30
+ │ └── components/ # subfeatures, com a mesma estrutura
31
+ └── shared/
32
+ ├── components/ # widgets reutilizáveis (buttons, inputs, widgets...)
33
+ ├── constraints/ # AppConstraints (bordas, breakpoints, limites)
34
+ ├── errors/ # Failure, exceções e Log
35
+ ├── infra/ # armazenamento local etc.
36
+ ├── models/
37
+ ├── providers/ # rotas e sessão
38
+ ├── services/ # implementações de infraestrutura
39
+ │ └── ports/ # contratos abstratos (HTTP, storage, push...)
40
+ ├── theme/ # paleta + extensão de ThemeData
41
+ ├── typography/ # AppTypography.of(context)
42
+ └── utils/
43
+ ```
44
+
45
+ 1. **Diretamente em `lib/`** só podem existir `components/`, `shared/`, `main.dart` e arquivos gerados de configuração (por exemplo, `firebase_options.dart`).
46
+ 2. **Na raiz de cada feature** (e de cada subfeature em `components/`), as únicas pastas permitidas são `models`, `providers`, `services`, `views` e `components`.
47
+ 3. **Use `components/` para subfeatures** quando a feature se divide por perfil ou área (por exemplo, `admin/components/reports`).
48
+
49
+ ## Camadas e fluxo de dados
50
+
51
+ ```
52
+ View ──context.read/watch──▶ Provider ──▶ Service ──▶ Port (shared/services/ports)
53
+ ▲ .run() + match/fold TaskEither<Failure, T> TaskEither<Exception, T> ▲
54
+ └──────── AppSnackBars ◀──── Log.print + Failure ◀───────────────────────────────┘
55
+ implementado em shared/services
56
+ ```
57
+
58
+ 4. **Ports** são classes `abstract` em `shared/services/ports/` e retornam `TaskEither<Exception, T>`. As implementações concretas ficam em `shared/services/`.
59
+ 5. **Services de feature** recebem *ports* pelo construtor, nunca a implementação concreta. Eles convertem JSON em model e retornam `TaskEither<Exception, T>`, sem estado, sem `notifyListeners` e sem `BuildContext`.
60
+ 6. **Providers** estendem `ChangeNotifier` e recebem o service pelo construtor. Guardam estado privado exposto por getters e retornam `TaskEither<Failure, T>` (normalmente `Unit`). Em erro, registram com `Log` e devolvem a `Failure` do domínio.
61
+ 7. **Views** usam `context.read<T>()` para ações e `context.watch<T>()` para estado. Executam com `.run()` e tratam o resultado com `.match(...)` ou `.fold(...)`.
62
+ 8. **fpdart em toda fronteira entre camadas:**
63
+ - Em `views → providers`, a view consome com `.run()` **e** `.match`, `.fold`, `.getOrElse`, `.isLeft` ou `.isRight`. Só `mapLeft` não basta, porque ignora o caminho de sucesso.
64
+ - Em `providers → services`, o arquivo importa `package:fpdart/fpdart.dart` e trabalha com `TaskEither` ou `Either`.
65
+ - Em `services → shared/services`, vale a mesma regra.
66
+ 9. **Direção única de dependência.** Uma view nunca importa um service, e um provider nunca importa uma view. A lógica de negócio fica fora dos widgets.
67
+ 10. **Crie uma camada de *use case*** só quando a lógica for complexa ou compartilhada entre providers. Caso contrário, ela fica no provider ou no service.
68
+
69
+ ## Regras de código
70
+
71
+ ### Documentação e estilo
72
+
73
+ 11. **Toda declaração tem `///` logo acima:** classes, construtores, métodos, getters, funções, enums, extensions, mixins, typedefs e campos de classe. Variáveis locais estão isentas. Siga o Effective Dart: primeira frase curta terminada em ponto, campos descritos com substantivo e booleanos começando com "Indica se...".
74
+ 12. **Não use `var`.** Use `final`, `const` ou tipo explícito.
75
+ 13. **Todo arquivo passa por `dart format`.** Código fora do formato não é commitado.
76
+ 14. **`flutter analyze --fatal-infos` precisa passar limpo.** Não use `// ignore` sem justificativa no próprio comentário.
77
+ 15. **Importe pelo caminho do pacote** (`package:<app>/...`). Use alias (`as admin`, `as customer`) quando houver nomes de classe iguais em features diferentes.
78
+ 16. **Ao mudar dependências, versione o `pubspec.lock`**, e no CI use `flutter pub get --enforce-lockfile`.
79
+
80
+ ### Models
81
+
82
+ 17. **Models são classes imutáveis**, com campos `final`, construtor `const` e `factory fromJson(Map<String, dynamic>)` defensivo (`?.toString() ?? ''`, `DateTime.tryParse`). Nada de `dynamic` vazando para fora do model.
83
+ 18. **Textos de exibição derivados** (rótulo, tradução, ícone) ficam em getters do model ou em `shared/utils`, nunca espalhados pela view.
84
+ 19. **DTOs e mappers** ficam em `services/dtos/` e `services/mappers/` quando o formato da API difere do model de domínio.
85
+
86
+ ### Erros e logs
87
+
88
+ 20. **`Failure` é uma `sealed class`** em `shared/errors/failure.dart`, com uma subclasse por domínio (`AuthFailure`, `LoadingFailure`, `ValidationFailure`, `GeneralFailure`...). Use a do domínio certo. Se faltar uma, crie a subclasse em vez de reaproveitar outra.
89
+ 21. **A mensagem da `Failure` é texto para o usuário**, claro e sem detalhe técnico. O detalhe técnico vai para o log, com `origin` (classe), `method`, `error` e `stack`.
90
+ 22. **Não use `print` nem `debugPrint` soltos.** Centralize em um `Log` com níveis (Debug, Info, Warning, Error, Critical) sobre `dart:developer`. Nunca registre tokens, senhas ou dados pessoais sensíveis.
91
+
92
+ ### Estado e injeção de dependência
93
+
94
+ 23. **A composição é explícita em `main.dart`.** Ela instancia os ports concretos, depois os services e por fim os providers, sempre por construtor. Não use service locator.
95
+ 24. **Escopo dos providers:**
96
+ - Globais (auth, sessão) ficam no `MultiProvider` acima do `MaterialApp.router`.
97
+ - Providers de uma área ficam com `ChangeNotifierProvider.value` no `ShellRoute` dessa área.
98
+ - Providers de tela efêmera são criados no `builder` da `GoRoute`.
99
+ 25. **Carga inicial** acontece em `initState` com `WidgetsBinding.instance.addPostFrameCallback((_) => _load())`.
100
+ 26. **Depois de qualquer `await` na view**, confira `if (!mounted) return;` (ou `context.mounted`) antes de usar o `context`.
101
+ 27. **Paralelize com `Future.wait`** somente chamadas independentes, e trate cada resultado com tipo explícito, sem `as List<T>`.
102
+ 28. **Exponha coleções como somente leitura** (`List.unmodifiable`) e só chame `notifyListeners()` quando o estado mudar de fato.
103
+
104
+ ### Navegação
105
+
106
+ 29. **Paths são constantes em uma classe `RoutesMapping`.** Nunca escreva string de rota solta na view.
107
+ 30. **Toda `GoRoute` tem `name`.** Navegue com `context.go(...)` ou `context.push(...)` usando `RoutesMapping`.
108
+ 31. **Autenticação e acesso** são decididos no `redirect` do `GoRouter`, com `refreshListenable` apontando para o provider de auth. Não faça checagem de login dentro das telas.
109
+ 32. **Áreas com navegação própria** (por perfil, com bottom bar) usam `ShellRoute` com `navigatorKey` dedicado.
110
+ 33. **Push notifications e deep links** passam por um único método do gerenciador de rotas.
111
+
112
+ ### UI, tema e acessibilidade
113
+
114
+ 34. **Cores vêm de uma `enum` de paleta** (com getter `color`) **e de uma extensão de `ThemeData`** (`Theme.of(context).primary`). Evite `Colors.*` e hexadecimais na view.
115
+ 35. **Textos usam `AppTypography.of(context).<estilo>(...)`**, que escolhe entre mobile e tablet. Evite `TextStyle(fontSize: ...)` solto.
116
+ 36. **Botões, inputs, modais e snackbars** usam os componentes de `shared/components` ou os estilos expostos pela extensão de tema. Raio de borda, breakpoints e limites ficam em `AppConstraints`.
117
+ 37. **Feedback ao usuário** passa por `AppSnackBars.showSuccess`, `showError` e `showWarning`.
118
+ 38. **Layout responsivo** decide com `AppConstraints.isMobile` ou `isTablet` no nível da tela, e com `LayoutBuilder` dentro dos widgets.
119
+ 39. **Widgets pequenos e `const`:** quebre a tela em widgets dentro de `views/components/` em vez de métodos `_buildX()` gigantes.
120
+ 40. **Acessibilidade:** ícones clicáveis têm `tooltip` ou `Semantics`, e ícones informativos têm `semanticLabel`.
121
+
122
+ ### Testes
123
+
124
+ 41. **Testes ficam em `test/`, espelhando `lib/`**, com sufixo `_test.dart`.
125
+ 42. **Services são testados com um *fake* do port** (uma classe que implementa o contrato). Providers são testados com o service real sobre esse port fake. Não acesse rede nem serviços externos em teste unitário.
126
+ 43. **Widgets são testados com `testWidgets`**, envolvendo a tela em `MaterialApp` e `ChangeNotifierProvider.value`. Use `pump` para mudanças simples e `pumpAndSettle` para animações e operações async.
127
+
128
+ ## Fluxo para criar uma feature
129
+
130
+ - [ ] **1. Pasta.** Crie `lib/components/<feature>/` com `models/`, `services/`, `providers/` e `views/`.
131
+ - [ ] **2. Model.** Crie a classe imutável com `fromJson` defensivo.
132
+ - [ ] **3. Port.** Se precisar de infraestrutura nova, crie o contrato em `shared/services/ports/` e a implementação em `shared/services/`.
133
+ - [ ] **4. Service.** Receba os ports pelo construtor e retorne `TaskEither<Exception, T>`.
134
+ - [ ] **5. Provider.** Estenda `ChangeNotifier`, guarde o estado privado e use `TaskEither.tryCatch`, `Log` e a `Failure` do domínio.
135
+ - [ ] **6. View.** Leia com `context.watch`, aja com `context.read` e consuma com `.run()` + `.match`.
136
+ - [ ] **7. Rota.** Adicione a constante em `RoutesMapping` e a `GoRoute` nomeada.
137
+ - [ ] **8. DI.** Instancie o service e o provider em `main.dart` e registre no escopo certo.
138
+ - [ ] **9. Testes.** Teste o service com port fake, o provider e o widget principal.
139
+ - [ ] **10. Validação:**
140
+
141
+ ```bash
142
+ dart format lib test
143
+ flutter analyze --fatal-infos
144
+ flutter test
145
+ ```
146
+
147
+ ## Exemplos (feature ilustrativa: tarefas)
148
+
149
+ ### Port
150
+
151
+ ```dart
152
+ // lib/shared/services/ports/app_http_port.dart
153
+ import 'package:fpdart/fpdart.dart';
154
+
155
+ /// Contrato de acesso HTTP à API do app.
156
+ abstract class AppHttpPort {
157
+ /// Executa um GET em [path] e converte a resposta com [fromJson].
158
+ TaskEither<Exception, T> get<T>(String path, T Function(dynamic) fromJson);
159
+ }
160
+ ```
161
+
162
+ ### Model
163
+
164
+ ```dart
165
+ // lib/components/tasks/models/task_model.dart
166
+
167
+ /// Representa uma tarefa do usuário.
168
+ class TaskModel {
169
+ /// Identificador da tarefa.
170
+ final String id;
171
+
172
+ /// Título exibido na lista.
173
+ final String title;
174
+
175
+ /// Data de conclusão, quando houver.
176
+ final DateTime? doneAt;
177
+
178
+ /// Cria uma tarefa imutável.
179
+ const TaskModel({required this.id, required this.title, this.doneAt});
180
+
181
+ /// Constrói a tarefa a partir do JSON da API.
182
+ factory TaskModel.fromJson(Map<String, dynamic> json) {
183
+ return TaskModel(
184
+ id: json['id']?.toString() ?? '',
185
+ title: json['title']?.toString() ?? '',
186
+ doneAt: DateTime.tryParse(json['done_at']?.toString() ?? ''),
187
+ );
188
+ }
189
+
190
+ /// Indica se a tarefa foi concluída.
191
+ bool get isDone => doneAt != null;
192
+ }
193
+ ```
194
+
195
+ ### Service
196
+
197
+ ```dart
198
+ // lib/components/tasks/services/tasks_service.dart
199
+ import 'package:fpdart/fpdart.dart';
200
+ import 'package:my_app/components/tasks/models/task_model.dart';
201
+ import 'package:my_app/shared/services/ports/app_http_port.dart';
202
+
203
+ /// Acessa os dados de tarefas.
204
+ class TasksService {
205
+ /// Porta HTTP para a API.
206
+ final AppHttpPort _httpPort;
207
+
208
+ /// Cria o serviço com a porta HTTP injetada.
209
+ TasksService(this._httpPort);
210
+
211
+ /// Busca as tarefas do usuário autenticado.
212
+ TaskEither<Exception, List<TaskModel>> getTasks() {
213
+ return _httpPort.get<List<TaskModel>>('/tasks', (json) {
214
+ final List<dynamic> list = json is List
215
+ ? json
216
+ : (json['data'] ?? <dynamic>[]) as List<dynamic>;
217
+ return list
218
+ .map((item) => TaskModel.fromJson(item as Map<String, dynamic>))
219
+ .toList();
220
+ });
221
+ }
222
+ }
223
+ ```
224
+
225
+ ### Provider
226
+
227
+ ```dart
228
+ // lib/components/tasks/providers/tasks_provider.dart
229
+ import 'package:flutter/foundation.dart';
230
+ import 'package:fpdart/fpdart.dart';
231
+ import 'package:my_app/components/tasks/models/task_model.dart';
232
+ import 'package:my_app/components/tasks/services/tasks_service.dart';
233
+ import 'package:my_app/shared/errors/failure.dart';
234
+ import 'package:my_app/shared/errors/log.dart';
235
+
236
+ /// Gerencia o estado da tela de tarefas.
237
+ class TasksProvider extends ChangeNotifier {
238
+ /// Serviço de acesso às tarefas.
239
+ final TasksService _service;
240
+
241
+ /// Cria o provider com o serviço injetado.
242
+ TasksProvider(this._service);
243
+
244
+ /// Tarefas carregadas.
245
+ List<TaskModel> _tasks = const [];
246
+
247
+ /// Indica se as tarefas estão sendo carregadas.
248
+ bool _isLoading = false;
249
+
250
+ /// Tarefas carregadas, somente leitura.
251
+ List<TaskModel> get tasks => List.unmodifiable(_tasks);
252
+
253
+ /// Indica se as tarefas estão sendo carregadas.
254
+ bool get isLoading => _isLoading;
255
+
256
+ /// Carrega as tarefas e notifica a tela.
257
+ TaskEither<Failure, Unit> load() {
258
+ return TaskEither.tryCatch(
259
+ () async {
260
+ _setLoading(true);
261
+ try {
262
+ final result = await _service.getTasks().run();
263
+ _tasks = result.getOrElse((error) => throw error);
264
+ return unit;
265
+ } finally {
266
+ _setLoading(false);
267
+ }
268
+ },
269
+ (error, stack) {
270
+ Log.print<Error>(
271
+ message: 'Erro ao carregar tarefas',
272
+ origin: 'TasksProvider',
273
+ method: 'load',
274
+ error: error,
275
+ stack: stack,
276
+ );
277
+ return const LoadingFailure('Não foi possível carregar suas tarefas.');
278
+ },
279
+ );
280
+ }
281
+
282
+ /// Atualiza o indicador de carregamento e notifica os ouvintes.
283
+ void _setLoading(bool value) {
284
+ _isLoading = value;
285
+ notifyListeners();
286
+ }
287
+ }
288
+ ```
289
+
290
+ ### View
291
+
292
+ ```dart
293
+ // lib/components/tasks/views/tasks_view.dart
294
+ import 'package:flutter/material.dart';
295
+ import 'package:my_app/components/tasks/providers/tasks_provider.dart';
296
+ import 'package:my_app/shared/components/widgets/app_snackbars.dart';
297
+ import 'package:my_app/shared/theme/app_theme.dart';
298
+ import 'package:my_app/shared/typography/app_typography.dart';
299
+ import 'package:provider/provider.dart';
300
+
301
+ /// Tela com a lista de tarefas.
302
+ class TasksView extends StatefulWidget {
303
+ /// Cria a tela de tarefas.
304
+ const TasksView({super.key});
305
+
306
+ /// Cria o estado da tela.
307
+ @override
308
+ State<TasksView> createState() => _TasksViewState();
309
+ }
310
+
311
+ /// Estado da tela de tarefas.
312
+ class _TasksViewState extends State<TasksView> {
313
+ /// Agenda a carga inicial após o primeiro frame.
314
+ @override
315
+ void initState() {
316
+ super.initState();
317
+ WidgetsBinding.instance.addPostFrameCallback((_) => _load());
318
+ }
319
+
320
+ /// Carrega as tarefas e mostra erro amigável em caso de falha.
321
+ Future<void> _load() async {
322
+ final result = await context.read<TasksProvider>().load().run();
323
+ if (!mounted) return;
324
+ result.match(
325
+ (failure) => AppSnackBars.showError(context, failure.message),
326
+ (_) {},
327
+ );
328
+ }
329
+
330
+ /// Monta a lista de tarefas.
331
+ @override
332
+ Widget build(BuildContext context) {
333
+ final provider = context.watch<TasksProvider>();
334
+ final typography = AppTypography.of(context);
335
+
336
+ if (provider.isLoading) {
337
+ return Center(
338
+ child: CircularProgressIndicator(color: Theme.of(context).primary),
339
+ );
340
+ }
341
+
342
+ return ListView.separated(
343
+ padding: const EdgeInsets.all(16),
344
+ itemCount: provider.tasks.length,
345
+ separatorBuilder: (_, _) => const SizedBox(height: 8),
346
+ itemBuilder: (context, index) {
347
+ final task = provider.tasks[index];
348
+ return ListTile(
349
+ title: Text(task.title, style: typography.topic()),
350
+ trailing: Icon(
351
+ task.isDone ? Icons.check_circle : Icons.schedule,
352
+ color: Theme.of(context).primary,
353
+ semanticLabel: task.isDone ? 'Concluída' : 'Pendente',
354
+ ),
355
+ );
356
+ },
357
+ );
358
+ }
359
+ }
360
+ ```
361
+
362
+ ### Teste do service com port fake
363
+
364
+ ```dart
365
+ // test/components/tasks/services/tasks_service_test.dart
366
+ import 'package:flutter_test/flutter_test.dart';
367
+ import 'package:fpdart/fpdart.dart';
368
+ import 'package:my_app/components/tasks/services/tasks_service.dart';
369
+ import 'package:my_app/shared/services/ports/app_http_port.dart';
370
+
371
+ /// Porta HTTP falsa que devolve uma resposta fixa.
372
+ class _FakeHttpPort implements AppHttpPort {
373
+ /// Cria a porta com a resposta a devolver.
374
+ _FakeHttpPort(this.response);
375
+
376
+ /// Resposta JSON simulada.
377
+ final Object response;
378
+
379
+ /// Devolve a resposta simulada convertida pelo [fromJson].
380
+ @override
381
+ TaskEither<Exception, T> get<T>(String path, T Function(dynamic) fromJson) =>
382
+ TaskEither.of(fromJson(response));
383
+ }
384
+
385
+ void main() {
386
+ test('converte a lista da API em TaskModel', () async {
387
+ final service = TasksService(
388
+ _FakeHttpPort({
389
+ 'data': [
390
+ {'id': '1', 'title': 'Revisar PR', 'done_at': '2026-05-10'},
391
+ ],
392
+ }),
393
+ );
394
+
395
+ final result = await service.getTasks().run();
396
+
397
+ final tasks = result.getOrElse((error) => throw error);
398
+ expect(tasks.single.title, 'Revisar PR');
399
+ expect(tasks.single.isDone, isTrue);
400
+ });
401
+ }
402
+ ```
403
+
404
+ ## Evite
405
+
406
+ - **`Failure` genérica ou errada:** por exemplo, `AuthFailure` para erro de carregamento.
407
+ - **Estilos fixos na view:** `Colors.white`, `TextStyle(fontSize: 16)`, `Colors.grey.shade300`.
408
+ - **Casts depois de `Future.wait`:** algo como `results[0] as List<T>`.
409
+ - **Import duplicado** do mesmo arquivo com e sem alias.
410
+ - **Declarações sem `///`.**
411
+ - **Consumo de `TaskEither` só com `mapLeft`**, que ignora o caminho de sucesso.
412
+ - **`try/catch` dentro da view** para tratar erro de provider. O erro já chega como `Failure`.
413
+
414
+ ## Referências
415
+
416
+ - Guia de arquitetura do Flutter: https://docs.flutter.dev/app-architecture
417
+ - Testes no Flutter: https://docs.flutter.dev/testing/overview
418
+ - Effective Dart: https://dart.dev/effective-dart
419
+ - Skills oficiais do Flutter: https://github.com/flutter/skills
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: hex-pattern-dart
3
+ description: Padrão hexagonal (ports & adapters) da DS9 para projetos Dart, independente de framework — organização por contexto em domain (models, ports, services) e adapter (controllers, dtos, repository), regra de dependência, camadas de erro fixas (BusinessException, ClientException, MiddlewareException), composição de dependências na borda e resultado tipado com TaskEither. Use ao criar, revisar ou refatorar módulos/features, definir camadas, ports, services, adapters ou repositórios em Dart, ou quando o usuário pedir "arquitetura hexagonal", "ports and adapters" ou "organização de novo contexto".
4
+ ---
5
+
6
+ # Padrão Hexagonal DS9 para Dart (`hex-pattern-dart`)
7
+
8
+ Base arquitetural dos projetos Dart da DS9. Isola a regra de negócio de protocolo, banco e framework, deixando o núcleo testável e substituível. Siga junto com a skill `dart` (nomenclatura, `///`, OOP). Para um framework específico, use também a skill dele (ex.: `dart-frog` aplica este padrão em APIs HTTP). Esta skill não depende de nenhum framework nem de projeto específico. **Escopo:** back-end e pacotes Dart de domínio. Apps Flutter seguem a skill `flutter` (organização própria em components/providers/services); dela aproveite só os princípios (regra de dependência, ports, `TaskEither`).
9
+
10
+ Os blocos de código nos arquivos complementares são **pseudocódigo** ilustrativo: adapte à sintaxe e às versões do projeto.
11
+
12
+ ## 1. Regras inegociáveis
13
+
14
+ 1. **Regra de dependência:** `adapter → domain`. O `domain` nunca importa `adapter`, framework, driver de banco nem tipos de protocolo (request/response).
15
+ 2. **Resultado tipado:** operações que podem falhar retornam `TaskEither<Exception, T>` (`fpdart`), criado com `TaskEither.tryCatch`. Não lance exceção através das camadas. Se `fpdart` não estiver no `pubspec.yaml`, informe que ele é obrigatório para seguir o padrão DS9 e peça autorização para instalar.
16
+ 3. **Composição na borda:** dependências são montadas em um único ponto (composition root) e injetadas por construtor. Nunca instancie service, repositório ou controller dentro da rota/handler.
17
+ 4. **Camadas de erro fixas:** `BusinessException` (domínio), `ClientException` (resposta ao cliente) e `MiddlewareException` (borda). Erro 5xx nunca expõe mensagem interna.
18
+ 5. **Sem regra de negócio no adapter:** controller traduz protocolo ↔ chamada do service; repositório só persiste e mapeia.
19
+ 6. **Projeto existente:** preserve a estrutura atual; aplique o padrão no código novo e no que for pedido, sem refatorar o restante.
20
+
21
+ ## 2. Estrutura por contexto
22
+
23
+ ```text
24
+ lib/
25
+ ├── <contexto>/ # bounded context (ex.: orders)
26
+ │ ├── domain/
27
+ │ │ ├── models/ # entidades e value objects (invariantes)
28
+ │ │ ├── ports/ # contratos que o domínio precisa (saída)
29
+ │ │ └── services/ # casos de uso; dependem só de ports e models
30
+ │ └── adapter/
31
+ │ ├── controllers/ # entrada: traduz protocolo → service → resposta
32
+ │ ├── dtos/ # contratos de entrada/saída + validação
33
+ │ └── repository/ # saída: implementa ports (persistência, clients)
34
+ └── common/ # exceções, tipos e utilitários compartilhados
35
+ ```
36
+
37
+ - Imports **relativos** dentro de `lib/`; o composition root e os testes usam `package:`.
38
+ - Um contexto não importa `domain`/`adapter` de outro. Comunique-se por port ou por tipos em `common/`.
39
+
40
+ ## 3. Responsabilidades
41
+
42
+ | Peça | Faz | Não faz |
43
+ |---|---|---|
44
+ | `model` | Invariantes, comportamento do negócio | I/O, serialização de protocolo |
45
+ | `port` | Declara o contrato (`abstract interface class`) | Implementação |
46
+ | `service` | Orquestra ports, aplica regra e autorização de recurso, retorna `TaskEither` | Conhece HTTP, SQL, DTO |
47
+ | `controller` | Valida DTO, chama service, mapeia erro de negócio → `ClientException` | Regra de negócio |
48
+ | `dto` | Entrada/saída do protocolo, `fromJson`/`toJson` | Ser usado no domínio |
49
+ | `repository` | Implementa port, mapeia modelo ↔ persistência | Regra de negócio |
50
+
51
+ ## 4. Erros por camada
52
+
53
+ - Repositório/service: `TaskEither.tryCatch` converte falha em `BusinessException` (com `domainName`, `statusCode`, mensagem segura, erro original e stack).
54
+ - Controller: converte `BusinessException` em `ClientException` (mensagem segura; 5xx genérico).
55
+ - Borda (middleware/handler): converte `ClientException`/`MiddlewareException` em resposta de protocolo.
56
+
57
+ ## 5. Checklist rápido
58
+
59
+ - [ ] O `domain` não importa nada de `adapter` nem de framework?
60
+ - [ ] Todo retorno que pode falhar é `TaskEither` via `tryCatch`?
61
+ - [ ] Dependências vêm do composition root por construtor?
62
+ - [ ] Os erros usam as 3 camadas e 5xx não vaza detalhe?
63
+ - [ ] Controller sem regra de negócio; repositório sem regra de negócio?
64
+ - [ ] DTO fica fora do `domain`?
65
+ - [ ] Existem testes do service com fakes das ports?
66
+
67
+ ## 6. Arquivos complementares (leitura sob demanda)
68
+
69
+ Use `list_skill_files` e `get_skill_file` com `skill: hex-pattern-dart`.
70
+
71
+ | Dúvida | Arquivo |
72
+ |---|---|
73
+ | Camadas e regra de dependência | `references/layers-and-dependency-rule.md` |
74
+ | Como desenhar ports e adapters | `references/ports-and-adapters.md` |
75
+ | Camadas de erro | `references/error-layers.md` |
76
+ | Injeção e composition root | `references/dependency-injection.md` |
77
+ | Persistência agnóstica e mapper | `references/persistence.md` |
78
+ | Estratégia de testes | `references/testing.md` |
79
+ | Erros comuns | `references/anti-patterns.md` |
80
+ | Checklist completo de review | `references/checklist.md` |
81
+ | Árvore de pastas de exemplo | `examples/structure-tree.md` |
82
+ | Port (`<Papel>Port`), service e adapters (pseudocódigo) | `examples/port.md`, `examples/service.md`, `examples/adapter-in.md`, `examples/adapter-out.md` |
83
+ | Hierarquia de exceções | `examples/errors.md` |
@@ -0,0 +1,24 @@
1
+ # Adapter de entrada — controller (pseudocódigo)
2
+
3
+ ```text
4
+ // adapter/controllers/order_controller.dart
5
+ /// Traduz o protocolo para o service e o resultado para a resposta.
6
+ class OrderController
7
+ final OrderService _service
8
+
9
+ OrderController(this._service)
10
+
11
+ Task<Response> getById(Context context, String id)
12
+ return _service.getById(id, context.requesterId)
13
+ .mapLeft(toClientException) // BusinessException -> ClientException
14
+ .match(
15
+ (client) => Response(status: client.statusCode, body: client.message),
16
+ (order) => Response.ok(OrderResponse.from(order)),
17
+ )
18
+
19
+ toClientException(BusinessException e)
20
+ safe = e.statusCode >= 500 ? "Erro interno" : e.message // 5xx genérico
21
+ return ClientException(statusCode: e.statusCode, message: safe)
22
+ ```
23
+
24
+ Pontos-chave: sem regra de negócio; service recebido por construtor; erro 5xx sem detalhe interno; o log guarda `e.error` e `e.stackTrace`.
@@ -0,0 +1,20 @@
1
+ # Adapter de saída — repository (pseudocódigo)
2
+
3
+ ```text
4
+ // adapter/repository/order_repository.dart
5
+ /// Implementa OrderRepositoryPort sobre a tecnologia de persistência do projeto.
6
+ class OrderRepository implements OrderRepositoryPort
7
+ final DataSource _source // abstração do acesso físico
8
+
9
+ OrderRepository(this._source)
10
+
11
+ TaskEither<BusinessException, Order> findById(String id)
12
+ return TaskEither.tryCatch(
13
+ () async => OrderMapper.toDomain(await _source.read(id)),
14
+ (error, stack) => BusinessException(
15
+ domainName: "orders", statusCode: 500,
16
+ message: "Falha ao consultar pedido", error: error, stackTrace: stack),
17
+ )
18
+ ```
19
+
20
+ Pontos-chave: toda chamada de I/O em `tryCatch`; mapper converte persistência → domínio; a falha vira `BusinessException` com mensagem segura.
@@ -0,0 +1,25 @@
1
+ # Hierarquia de exceções (pseudocódigo)
2
+
3
+ ```text
4
+ // common/exceptions
5
+ /// Falha de regra de negócio ou de infraestrutura vista pelo domínio.
6
+ class BusinessException implements Exception
7
+ String domainName
8
+ int statusCode
9
+ String message // segura para o cliente
10
+ Object? error // causa original (somente log)
11
+ StackTrace? stackTrace
12
+
13
+ /// Falha pronta para ser devolvida ao cliente.
14
+ class ClientException implements Exception
15
+ int statusCode
16
+ String message
17
+
18
+ /// Falha da borda (autenticação, parsing, composição).
19
+ class MiddlewareException implements Exception
20
+ int statusCode
21
+ String message
22
+ StackTrace? stackTrace
23
+ ```
24
+
25
+ Regra: `BusinessException` → (controller) → `ClientException`. A borda converte qualquer uma das duas, ou `MiddlewareException`, em resposta de protocolo.
@@ -0,0 +1,11 @@
1
+ # Port (pseudocódigo)
2
+
3
+ ```text
4
+ // domain/ports/order_repository.dart
5
+ /// Contrato de persistência de pedidos exigido pelo domínio.
6
+ abstract interface class OrderRepositoryPort
7
+ TaskEither<BusinessException, Order> findById(String id)
8
+ TaskEither<BusinessException, Unit> save(Order order)
9
+ ```
10
+
11
+ Pontos-chave: só depende de `Order` e de `BusinessException`; nenhuma menção a banco, SQL ou protocolo.
@@ -0,0 +1,20 @@
1
+ # Service (pseudocódigo)
2
+
3
+ ```text
4
+ // domain/services/order_service.dart
5
+ /// Casos de uso de pedidos.
6
+ class OrderService
7
+ final OrderRepositoryPort _repository // recebido por construtor
8
+
9
+ OrderService(this._repository)
10
+
11
+ TaskEither<BusinessException, Order> getById(String id, String requesterId)
12
+ return _repository.findById(id).flatMap((order) =>
13
+ order.belongsTo(requesterId) // autorização de recurso aqui
14
+ ? TaskEither.right(order)
15
+ : TaskEither.left(BusinessException(
16
+ domainName: "orders", statusCode: 403, message: "Acesso negado"))
17
+ )
18
+ ```
19
+
20
+ Pontos-chave: depende só da port; sem DTO, sem protocolo; regra e autorização no service; retorno `TaskEither`.
@@ -0,0 +1,28 @@
1
+ # Árvore de pastas (exemplo)
2
+
3
+ ```text
4
+ lib/
5
+ ├── common/
6
+ │ ├── exceptions/
7
+ │ │ ├── business_exception.dart
8
+ │ │ ├── client_exception.dart
9
+ │ │ └── middleware_exception.dart
10
+ │ └── error_response.dart
11
+ └── orders/
12
+ ├── domain/
13
+ │ ├── models/order.dart
14
+ │ ├── ports/order_repository.dart
15
+ │ └── services/order_service.dart
16
+ └── adapter/
17
+ ├── controllers/order_controller.dart
18
+ ├── dtos/create_order_dto.dart
19
+ └── repository/
20
+ ├── order_mapper.dart
21
+ └── order_repository.dart
22
+ test/
23
+ └── orders/
24
+ ├── domain/services/order_service_test.dart
25
+ └── adapter/controllers/order_controller_test.dart
26
+ ```
27
+
28
+ Adicione um novo contexto criando a mesma árvore sob `lib/<contexto>/`.
@@ -0,0 +1,16 @@
1
+ # Anti-padrões
2
+
3
+ | Anti-padrão | Por que é problema | Correção |
4
+ |---|---|---|
5
+ | Instanciar controller/service dentro da rota | Impede troca e teste; acopla borda ao concreto | Injetar pelo composition root |
6
+ | Regra de negócio em middleware/controller | Some em testes de domínio; duplica em outros adapters | Mover para o service |
7
+ | Ler segmento de path por posição para autorizar | Frágil e fácil de burlar | Autorizar no service com o identificador do recurso |
8
+ | `error.toString()` na resposta | Vaza detalhe interno ao cliente | Mensagem segura em `ClientException` |
9
+ | `catch (_) {}` genérico | Engole a causa e o stack | `TaskEither.tryCatch` + `BusinessException` |
10
+ | DTO usado no `domain` | Acopla negócio ao protocolo | DTO só em `adapter/dtos` |
11
+ | Model de persistência exposto ao `domain` | Acopla negócio ao banco | Mapper no repositório |
12
+ | Service que acessa banco/HTTP direto | Quebra a regra de dependência | Criar port e adapter |
13
+ | Service "faz tudo" (god service) | Difícil de testar e evoluir | Um service por caso de uso/agregado |
14
+ | Contexto importando `domain` de outro | Acoplamento entre contextos | Port ou tipo em `common/` |
15
+ | Exceção lançada entre camadas | Fluxo de erro invisível no tipo | Retornar `TaskEither` |
16
+ | `null` como "não encontrado" | Ambíguo | `Option`/`left` explícito |