cad-workflow 1.1.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/bin/cli.js +529 -0
  4. package/bin/wrapper.js +32 -0
  5. package/config/install-config.yaml +167 -0
  6. package/package.json +42 -0
  7. package/src/base/.cad/config.yaml.tpl +25 -0
  8. package/src/base/.cad/workflow-status.yaml.tpl +18 -0
  9. package/src/base/.claude/settings.local.json.tpl +8 -0
  10. package/src/base/CLAUDE.md +69 -0
  11. package/src/base/commands/cad.md +547 -0
  12. package/src/base/commands/commit.md +103 -0
  13. package/src/base/commands/comprendre.md +96 -0
  14. package/src/base/commands/concevoir.md +121 -0
  15. package/src/base/commands/documenter.md +97 -0
  16. package/src/base/commands/e2e.md +79 -0
  17. package/src/base/commands/implementer.md +98 -0
  18. package/src/base/commands/review.md +85 -0
  19. package/src/base/commands/status.md +55 -0
  20. package/src/base/skills/clean-code/SKILL.md +92 -0
  21. package/src/base/skills/tdd/SKILL.md +132 -0
  22. package/src/integrations/jira/.mcp.json.tpl +19 -0
  23. package/src/integrations/jira/commands/jira-setup.md +34 -0
  24. package/src/stacks/backend-only/agents/backend-developer.md +167 -0
  25. package/src/stacks/backend-only/agents/backend-reviewer.md +89 -0
  26. package/src/stacks/backend-only/agents/orchestrator.md +69 -0
  27. package/src/stacks/backend-only/skills/clean-hexa-backend/SKILL.md +187 -0
  28. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
  29. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
  30. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
  31. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/port.template.ts +62 -0
  32. package/src/stacks/backend-only/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
  33. package/src/stacks/backend-only/skills/mutation-testing/SKILL.md +129 -0
  34. package/src/stacks/mobile/agents/backend-developer.md +167 -0
  35. package/src/stacks/mobile/agents/backend-reviewer.md +89 -0
  36. package/src/stacks/mobile/agents/mobile-developer.md +70 -0
  37. package/src/stacks/mobile/agents/mobile-reviewer.md +175 -0
  38. package/src/stacks/mobile/agents/orchestrator.md +69 -0
  39. package/src/stacks/mobile/skills/clean-hexa-backend/SKILL.md +187 -0
  40. package/src/stacks/mobile/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
  41. package/src/stacks/mobile/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
  42. package/src/stacks/mobile/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
  43. package/src/stacks/mobile/skills/clean-hexa-backend/templates/port.template.ts +62 -0
  44. package/src/stacks/mobile/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
  45. package/src/stacks/mobile/skills/clean-hexa-mobile/SKILL.md +984 -0
  46. package/src/stacks/mobile/skills/mutation-testing/SKILL.md +129 -0
  47. package/src/stacks/web/agents/backend-developer.md +167 -0
  48. package/src/stacks/web/agents/backend-reviewer.md +89 -0
  49. package/src/stacks/web/agents/frontend-developer.md +65 -0
  50. package/src/stacks/web/agents/frontend-reviewer.md +92 -0
  51. package/src/stacks/web/agents/orchestrator.md +69 -0
  52. package/src/stacks/web/skills/clean-hexa-backend/SKILL.md +187 -0
  53. package/src/stacks/web/skills/clean-hexa-backend/templates/adapter.template.ts +75 -0
  54. package/src/stacks/web/skills/clean-hexa-backend/templates/controller.template.ts +131 -0
  55. package/src/stacks/web/skills/clean-hexa-backend/templates/entity.template.ts +87 -0
  56. package/src/stacks/web/skills/clean-hexa-backend/templates/port.template.ts +62 -0
  57. package/src/stacks/web/skills/clean-hexa-backend/templates/use-case.template.ts +77 -0
  58. package/src/stacks/web/skills/clean-hexa-frontend/SKILL.md +172 -0
  59. package/src/stacks/web/skills/mutation-testing/SKILL.md +129 -0
@@ -0,0 +1,984 @@
1
+ ---
2
+ name: clean-hexa-mobile
3
+ description: Clean Architecture Hexagonale pour React Native Expo. Patterns, exemples et templates pour structurer une app mobile.
4
+ ---
5
+
6
+ # Clean Hexa Mobile - React Native Expo
7
+
8
+ ## Principes fondamentaux
9
+
10
+ 1. **Domain au centre** : Entités pures TypeScript, zéro dépendance React/Expo
11
+ 2. **Use Cases = Custom Hooks** : Orchestration de la logique métier via hooks
12
+ 3. **Injection via Context** : Pas de singletons, testabilité maximale
13
+ 4. **Séparation Smart/Dumb** : Components présentationnels purs et réutilisables
14
+ 5. **Ports & Adapters** : Interfaces dans le domain, implémentations dans infrastructure
15
+
16
+ ## Structure de dossiers
17
+
18
+ ```
19
+ src/
20
+ ├── domain/ # Coeur métier - AUCUNE dépendance externe
21
+ │ ├── entities/ # Objets métier avec identité
22
+ │ │ └── todo.entity.ts
23
+ │ ├── value-objects/ # Objets immuables sans identité
24
+ │ │ └── todo-title.vo.ts
25
+ │ ├── ports/ # Interfaces (contrats)
26
+ │ │ └── todo.repository.port.ts
27
+ │ └── errors/ # Erreurs métier typées
28
+ │ └── todo-not-found.error.ts
29
+ ├── application/ # Orchestration
30
+ │ ├── use-cases/ # Custom hooks métier
31
+ │ │ ├── get-all-todos.use-case.ts
32
+ │ │ ├── get-all-todos.use-case.test.ts
33
+ │ │ ├── create-todo.use-case.ts
34
+ │ │ └── create-todo.use-case.test.ts
35
+ │ ├── dtos/ # Data Transfer Objects
36
+ │ │ ├── todo.dto.ts
37
+ │ │ └── create-todo.dto.ts
38
+ │ └── mappers/ # Conversions Entity <-> DTO
39
+ │ └── todo.mapper.ts
40
+ ├── infrastructure/ # Implémentations techniques
41
+ │ ├── adapters/ # Implémentations des ports
42
+ │ │ ├── api-todo.adapter.ts
43
+ │ │ └── api-todo.adapter.test.ts
44
+ │ ├── persistence/ # Stockage local
45
+ │ │ └── async-storage-todo.adapter.ts
46
+ │ ├── di/ # Dependency Injection
47
+ │ │ └── container.tsx
48
+ │ └── services/ # Services externes
49
+ │ └── api.client.ts
50
+ └── presentation/ # Interface utilisateur
51
+ ├── components/
52
+ │ ├── smart/ # Connectés aux use cases
53
+ │ │ └── todo-list.container.tsx
54
+ │ └── dumb/ # Props only, purs
55
+ │ ├── todo-item.component.tsx
56
+ │ └── todo-item.component.test.tsx
57
+ └── hooks/ # Hooks UI (theme, navigation, etc.)
58
+ └── use-theme.ts
59
+
60
+ app/ # Expo Router - Routes uniquement
61
+ ├── _layout.tsx # Root layout + providers
62
+ ├── index.tsx # Home screen
63
+ ├── (tabs)/
64
+ │ ├── _layout.tsx
65
+ │ └── todos.tsx
66
+ └── todo/
67
+ └── [id].tsx
68
+ ```
69
+
70
+ ## Règle des dépendances
71
+
72
+ Les imports vont TOUJOURS vers le centre :
73
+
74
+ ```
75
+ presentation → application → domain ← infrastructure
76
+ ↓ ↓ ↑
77
+ └──────────────┴───────────┘
78
+ ```
79
+
80
+ - presentation importe application
81
+ - application importe domain
82
+ - infrastructure importe domain (pour implémenter les ports)
83
+ - presentation importe infrastructure (pour le DI container)
84
+ - domain n'importe JAMAIS rien d'externe
85
+ - application n'importe JAMAIS infrastructure
86
+
87
+ ## Patterns détaillés
88
+
89
+ ### 1. Entity (Domain)
90
+
91
+ ```typescript
92
+ // domain/entities/todo.entity.ts
93
+
94
+ export interface TodoProps {
95
+ id: string;
96
+ title: string;
97
+ completed: boolean;
98
+ createdAt: Date;
99
+ }
100
+
101
+ export class Todo {
102
+ public readonly id: string;
103
+ public readonly title: string;
104
+ public readonly completed: boolean;
105
+ public readonly createdAt: Date;
106
+
107
+ private constructor(props: TodoProps) {
108
+ this.id = props.id;
109
+ this.title = props.title;
110
+ this.completed = props.completed;
111
+ this.createdAt = props.createdAt;
112
+ }
113
+
114
+ static create(title: string): Todo {
115
+ if (!title || title.trim().length === 0) {
116
+ throw new Error('Todo title cannot be empty');
117
+ }
118
+ return new Todo({
119
+ id: crypto.randomUUID(),
120
+ title: title.trim(),
121
+ completed: false,
122
+ createdAt: new Date(),
123
+ });
124
+ }
125
+
126
+ static fromProps(props: TodoProps): Todo {
127
+ return new Todo(props);
128
+ }
129
+
130
+ complete(): Todo {
131
+ return new Todo({ ...this, completed: true });
132
+ }
133
+
134
+ uncomplete(): Todo {
135
+ return new Todo({ ...this, completed: false });
136
+ }
137
+
138
+ updateTitle(newTitle: string): Todo {
139
+ if (!newTitle || newTitle.trim().length === 0) {
140
+ throw new Error('Todo title cannot be empty');
141
+ }
142
+ return new Todo({ ...this, title: newTitle.trim() });
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### 2. Port (Domain)
148
+
149
+ ```typescript
150
+ // domain/ports/todo.repository.port.ts
151
+
152
+ import { Todo } from '../entities/todo.entity';
153
+
154
+ export interface ITodoRepository {
155
+ findAll(): Promise<Todo[]>;
156
+ findById(id: string): Promise<Todo | null>;
157
+ save(todo: Todo): Promise<void>;
158
+ update(todo: Todo): Promise<void>;
159
+ delete(id: string): Promise<void>;
160
+ }
161
+ ```
162
+
163
+ ### 3. Domain Error
164
+
165
+ ```typescript
166
+ // domain/errors/todo-not-found.error.ts
167
+
168
+ export class TodoNotFoundError extends Error {
169
+ constructor(id: string) {
170
+ super(`Todo with id "${id}" not found`);
171
+ this.name = 'TodoNotFoundError';
172
+ }
173
+ }
174
+ ```
175
+
176
+ ### 4. DTO (Application)
177
+
178
+ ```typescript
179
+ // application/dtos/todo.dto.ts
180
+
181
+ export interface TodoDto {
182
+ id: string;
183
+ title: string;
184
+ completed: boolean;
185
+ createdAt: string;
186
+ }
187
+
188
+ export interface CreateTodoDto {
189
+ title: string;
190
+ }
191
+
192
+ export interface UpdateTodoDto {
193
+ title?: string;
194
+ completed?: boolean;
195
+ }
196
+ ```
197
+
198
+ ### 5. Mapper (Application)
199
+
200
+ ```typescript
201
+ // application/mappers/todo.mapper.ts
202
+
203
+ import { Todo } from '@/domain/entities/todo.entity';
204
+ import { TodoDto } from '../dtos/todo.dto';
205
+
206
+ export class TodoMapper {
207
+ static toDto(entity: Todo): TodoDto {
208
+ return {
209
+ id: entity.id,
210
+ title: entity.title,
211
+ completed: entity.completed,
212
+ createdAt: entity.createdAt.toISOString(),
213
+ };
214
+ }
215
+
216
+ static toDtoList(entities: Todo[]): TodoDto[] {
217
+ return entities.map(this.toDto);
218
+ }
219
+
220
+ static toEntity(dto: TodoDto): Todo {
221
+ return Todo.fromProps({
222
+ id: dto.id,
223
+ title: dto.title,
224
+ completed: dto.completed,
225
+ createdAt: new Date(dto.createdAt),
226
+ });
227
+ }
228
+ }
229
+ ```
230
+
231
+ ### 6. Use Case Hook (Application)
232
+
233
+ ```typescript
234
+ // application/use-cases/get-all-todos.use-case.ts
235
+
236
+ import { useState, useCallback } from 'react';
237
+ import { ITodoRepository } from '@/domain/ports/todo.repository.port';
238
+ import { TodoDto } from '../dtos/todo.dto';
239
+ import { TodoMapper } from '../mappers/todo.mapper';
240
+
241
+ interface UseGetAllTodosState {
242
+ todos: TodoDto[];
243
+ loading: boolean;
244
+ error: Error | null;
245
+ }
246
+
247
+ interface UseGetAllTodosReturn extends UseGetAllTodosState {
248
+ execute: () => Promise<void>;
249
+ reset: () => void;
250
+ }
251
+
252
+ export const useGetAllTodos = (repository: ITodoRepository): UseGetAllTodosReturn => {
253
+ const [state, setState] = useState<UseGetAllTodosState>({
254
+ todos: [],
255
+ loading: false,
256
+ error: null,
257
+ });
258
+
259
+ const execute = useCallback(async () => {
260
+ setState(prev => ({ ...prev, loading: true, error: null }));
261
+ try {
262
+ const entities = await repository.findAll();
263
+ const dtos = TodoMapper.toDtoList(entities);
264
+ setState({ todos: dtos, loading: false, error: null });
265
+ } catch (error) {
266
+ setState(prev => ({ ...prev, loading: false, error: error as Error }));
267
+ }
268
+ }, [repository]);
269
+
270
+ const reset = useCallback(() => {
271
+ setState({ todos: [], loading: false, error: null });
272
+ }, []);
273
+
274
+ return { ...state, execute, reset };
275
+ };
276
+ ```
277
+
278
+ ```typescript
279
+ // application/use-cases/create-todo.use-case.ts
280
+
281
+ import { useState, useCallback } from 'react';
282
+ import { ITodoRepository } from '@/domain/ports/todo.repository.port';
283
+ import { Todo } from '@/domain/entities/todo.entity';
284
+ import { CreateTodoDto, TodoDto } from '../dtos/todo.dto';
285
+ import { TodoMapper } from '../mappers/todo.mapper';
286
+
287
+ interface UseCreateTodoState {
288
+ createdTodo: TodoDto | null;
289
+ loading: boolean;
290
+ error: Error | null;
291
+ success: boolean;
292
+ }
293
+
294
+ interface UseCreateTodoReturn extends UseCreateTodoState {
295
+ execute: (dto: CreateTodoDto) => Promise<void>;
296
+ reset: () => void;
297
+ }
298
+
299
+ export const useCreateTodo = (repository: ITodoRepository): UseCreateTodoReturn => {
300
+ const [state, setState] = useState<UseCreateTodoState>({
301
+ createdTodo: null,
302
+ loading: false,
303
+ error: null,
304
+ success: false,
305
+ });
306
+
307
+ const execute = useCallback(async (dto: CreateTodoDto) => {
308
+ setState(prev => ({ ...prev, loading: true, error: null, success: false }));
309
+ try {
310
+ const entity = Todo.create(dto.title);
311
+ await repository.save(entity);
312
+ const todoDto = TodoMapper.toDto(entity);
313
+ setState({ createdTodo: todoDto, loading: false, error: null, success: true });
314
+ } catch (error) {
315
+ setState(prev => ({ ...prev, loading: false, error: error as Error, success: false }));
316
+ }
317
+ }, [repository]);
318
+
319
+ const reset = useCallback(() => {
320
+ setState({ createdTodo: null, loading: false, error: null, success: false });
321
+ }, []);
322
+
323
+ return { ...state, execute, reset };
324
+ };
325
+ ```
326
+
327
+ ### 7. Use Case Test
328
+
329
+ ```typescript
330
+ // application/use-cases/get-all-todos.use-case.test.ts
331
+
332
+ import { renderHook, act, waitFor } from '@testing-library/react-native';
333
+ import { useGetAllTodos } from './get-all-todos.use-case';
334
+ import { ITodoRepository } from '@/domain/ports/todo.repository.port';
335
+ import { Todo } from '@/domain/entities/todo.entity';
336
+
337
+ describe('useGetAllTodos', () => {
338
+ const createMockRepository = (): jest.Mocked<ITodoRepository> => ({
339
+ findAll: jest.fn(),
340
+ findById: jest.fn(),
341
+ save: jest.fn(),
342
+ update: jest.fn(),
343
+ delete: jest.fn(),
344
+ });
345
+
346
+ it('should return empty array initially', () => {
347
+ const mockRepo = createMockRepository();
348
+ const { result } = renderHook(() => useGetAllTodos(mockRepo));
349
+
350
+ expect(result.current.todos).toEqual([]);
351
+ expect(result.current.loading).toBe(false);
352
+ expect(result.current.error).toBeNull();
353
+ });
354
+
355
+ it('should fetch todos successfully', async () => {
356
+ const mockRepo = createMockRepository();
357
+ const mockTodos = [
358
+ Todo.fromProps({ id: '1', title: 'Test 1', completed: false, createdAt: new Date() }),
359
+ Todo.fromProps({ id: '2', title: 'Test 2', completed: true, createdAt: new Date() }),
360
+ ];
361
+ mockRepo.findAll.mockResolvedValue(mockTodos);
362
+
363
+ const { result } = renderHook(() => useGetAllTodos(mockRepo));
364
+
365
+ await act(async () => {
366
+ await result.current.execute();
367
+ });
368
+
369
+ await waitFor(() => {
370
+ expect(result.current.loading).toBe(false);
371
+ });
372
+
373
+ expect(result.current.todos).toHaveLength(2);
374
+ expect(result.current.error).toBeNull();
375
+ expect(mockRepo.findAll).toHaveBeenCalledTimes(1);
376
+ });
377
+
378
+ it('should handle error', async () => {
379
+ const mockRepo = createMockRepository();
380
+ const error = new Error('Network error');
381
+ mockRepo.findAll.mockRejectedValue(error);
382
+
383
+ const { result } = renderHook(() => useGetAllTodos(mockRepo));
384
+
385
+ await act(async () => {
386
+ await result.current.execute();
387
+ });
388
+
389
+ await waitFor(() => {
390
+ expect(result.current.loading).toBe(false);
391
+ });
392
+
393
+ expect(result.current.error).toBe(error);
394
+ expect(result.current.todos).toEqual([]);
395
+ });
396
+ });
397
+ ```
398
+
399
+ ### 8. Adapter (Infrastructure)
400
+
401
+ ```typescript
402
+ // infrastructure/adapters/api-todo.adapter.ts
403
+
404
+ import { ITodoRepository } from '@/domain/ports/todo.repository.port';
405
+ import { Todo } from '@/domain/entities/todo.entity';
406
+ import { apiClient } from '../services/api.client';
407
+
408
+ interface ApiTodoResponse {
409
+ id: string;
410
+ title: string;
411
+ completed: boolean;
412
+ createdAt: string;
413
+ }
414
+
415
+ export class ApiTodoAdapter implements ITodoRepository {
416
+ async findAll(): Promise<Todo[]> {
417
+ const response = await apiClient.get<ApiTodoResponse[]>('/todos');
418
+ return response.map(item => Todo.fromProps({
419
+ id: item.id,
420
+ title: item.title,
421
+ completed: item.completed,
422
+ createdAt: new Date(item.createdAt),
423
+ }));
424
+ }
425
+
426
+ async findById(id: string): Promise<Todo | null> {
427
+ try {
428
+ const response = await apiClient.get<ApiTodoResponse>(`/todos/${id}`);
429
+ return Todo.fromProps({
430
+ id: response.id,
431
+ title: response.title,
432
+ completed: response.completed,
433
+ createdAt: new Date(response.createdAt),
434
+ });
435
+ } catch {
436
+ return null;
437
+ }
438
+ }
439
+
440
+ async save(todo: Todo): Promise<void> {
441
+ await apiClient.post('/todos', {
442
+ id: todo.id,
443
+ title: todo.title,
444
+ completed: todo.completed,
445
+ createdAt: todo.createdAt.toISOString(),
446
+ });
447
+ }
448
+
449
+ async update(todo: Todo): Promise<void> {
450
+ await apiClient.put(`/todos/${todo.id}`, {
451
+ title: todo.title,
452
+ completed: todo.completed,
453
+ });
454
+ }
455
+
456
+ async delete(id: string): Promise<void> {
457
+ await apiClient.delete(`/todos/${id}`);
458
+ }
459
+ }
460
+ ```
461
+
462
+ ```typescript
463
+ // infrastructure/persistence/in-memory-todo.adapter.ts
464
+
465
+ import { ITodoRepository } from '@/domain/ports/todo.repository.port';
466
+ import { Todo } from '@/domain/entities/todo.entity';
467
+
468
+ export class InMemoryTodoAdapter implements ITodoRepository {
469
+ private todos: Map<string, Todo> = new Map();
470
+
471
+ async findAll(): Promise<Todo[]> {
472
+ return Array.from(this.todos.values());
473
+ }
474
+
475
+ async findById(id: string): Promise<Todo | null> {
476
+ return this.todos.get(id) ?? null;
477
+ }
478
+
479
+ async save(todo: Todo): Promise<void> {
480
+ this.todos.set(todo.id, todo);
481
+ }
482
+
483
+ async update(todo: Todo): Promise<void> {
484
+ if (!this.todos.has(todo.id)) {
485
+ throw new Error(`Todo ${todo.id} not found`);
486
+ }
487
+ this.todos.set(todo.id, todo);
488
+ }
489
+
490
+ async delete(id: string): Promise<void> {
491
+ this.todos.delete(id);
492
+ }
493
+
494
+ // Helper pour les tests
495
+ clear(): void {
496
+ this.todos.clear();
497
+ }
498
+
499
+ seed(todos: Todo[]): void {
500
+ todos.forEach(todo => this.todos.set(todo.id, todo));
501
+ }
502
+ }
503
+ ```
504
+
505
+ ### 9. API Client (Infrastructure)
506
+
507
+ ```typescript
508
+ // infrastructure/services/api.client.ts
509
+
510
+ const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:3000/api';
511
+
512
+ class ApiClient {
513
+ private baseUrl: string;
514
+
515
+ constructor(baseUrl: string) {
516
+ this.baseUrl = baseUrl;
517
+ }
518
+
519
+ private async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
520
+ const url = `${this.baseUrl}${endpoint}`;
521
+ const response = await fetch(url, {
522
+ ...options,
523
+ headers: {
524
+ 'Content-Type': 'application/json',
525
+ ...options?.headers,
526
+ },
527
+ });
528
+
529
+ if (!response.ok) {
530
+ throw new Error(`API Error: ${response.status} ${response.statusText}`);
531
+ }
532
+
533
+ return response.json();
534
+ }
535
+
536
+ async get<T>(endpoint: string): Promise<T> {
537
+ return this.request<T>(endpoint, { method: 'GET' });
538
+ }
539
+
540
+ async post<T>(endpoint: string, data: unknown): Promise<T> {
541
+ return this.request<T>(endpoint, {
542
+ method: 'POST',
543
+ body: JSON.stringify(data),
544
+ });
545
+ }
546
+
547
+ async put<T>(endpoint: string, data: unknown): Promise<T> {
548
+ return this.request<T>(endpoint, {
549
+ method: 'PUT',
550
+ body: JSON.stringify(data),
551
+ });
552
+ }
553
+
554
+ async delete<T>(endpoint: string): Promise<T> {
555
+ return this.request<T>(endpoint, { method: 'DELETE' });
556
+ }
557
+ }
558
+
559
+ export const apiClient = new ApiClient(API_BASE_URL);
560
+ ```
561
+
562
+ ### 10. Container DI (Infrastructure)
563
+
564
+ ```typescript
565
+ // infrastructure/di/container.tsx
566
+
567
+ import React, { createContext, useContext, useMemo, ReactNode } from 'react';
568
+ import { ITodoRepository } from '@/domain/ports/todo.repository.port';
569
+ import { ApiTodoAdapter } from '../adapters/api-todo.adapter';
570
+ import { InMemoryTodoAdapter } from '../persistence/in-memory-todo.adapter';
571
+
572
+ interface Container {
573
+ todoRepository: ITodoRepository;
574
+ }
575
+
576
+ const ContainerContext = createContext<Container | null>(null);
577
+
578
+ interface ContainerProviderProps {
579
+ children: ReactNode;
580
+ overrides?: Partial<Container>;
581
+ }
582
+
583
+ export const ContainerProvider: React.FC<ContainerProviderProps> = ({
584
+ children,
585
+ overrides
586
+ }) => {
587
+ const container = useMemo<Container>(() => ({
588
+ todoRepository: overrides?.todoRepository ?? new ApiTodoAdapter(),
589
+ }), [overrides]);
590
+
591
+ return (
592
+ <ContainerContext.Provider value={container}>
593
+ {children}
594
+ </ContainerContext.Provider>
595
+ );
596
+ };
597
+
598
+ export const useContainer = (): Container => {
599
+ const container = useContext(ContainerContext);
600
+ if (!container) {
601
+ throw new Error('useContainer must be used within a ContainerProvider');
602
+ }
603
+ return container;
604
+ };
605
+
606
+ // Hooks raccourcis pour chaque dépendance
607
+ export const useTodoRepository = (): ITodoRepository => {
608
+ return useContainer().todoRepository;
609
+ };
610
+
611
+ // Provider pour les tests avec mocks
612
+ export const TestContainerProvider: React.FC<ContainerProviderProps> = ({
613
+ children,
614
+ overrides
615
+ }) => {
616
+ const container = useMemo<Container>(() => ({
617
+ todoRepository: overrides?.todoRepository ?? new InMemoryTodoAdapter(),
618
+ }), [overrides]);
619
+
620
+ return (
621
+ <ContainerContext.Provider value={container}>
622
+ {children}
623
+ </ContainerContext.Provider>
624
+ );
625
+ };
626
+ ```
627
+
628
+ ### 11. Smart Component (Presentation)
629
+
630
+ ```typescript
631
+ // presentation/components/smart/todo-list.container.tsx
632
+
633
+ import React, { useEffect, useCallback } from 'react';
634
+ import { View, StyleSheet, RefreshControl } from 'react-native';
635
+ import { useTodoRepository } from '@/infrastructure/di/container';
636
+ import { useGetAllTodos } from '@/application/use-cases/get-all-todos.use-case';
637
+ import { useDeleteTodo } from '@/application/use-cases/delete-todo.use-case';
638
+ import { useToggleTodo } from '@/application/use-cases/toggle-todo.use-case';
639
+ import { TodoList } from '../dumb/todo-list.component';
640
+ import { LoadingSpinner } from '../dumb/loading-spinner.component';
641
+ import { ErrorMessage } from '../dumb/error-message.component';
642
+ import { EmptyState } from '../dumb/empty-state.component';
643
+
644
+ export const TodoListContainer: React.FC = () => {
645
+ const repository = useTodoRepository();
646
+ const { todos, loading, error, execute: fetchTodos } = useGetAllTodos(repository);
647
+ const { execute: deleteTodo } = useDeleteTodo(repository);
648
+ const { execute: toggleTodo } = useToggleTodo(repository);
649
+
650
+ useEffect(() => {
651
+ fetchTodos();
652
+ }, [fetchTodos]);
653
+
654
+ const handleToggle = useCallback(async (id: string) => {
655
+ await toggleTodo(id);
656
+ await fetchTodos();
657
+ }, [toggleTodo, fetchTodos]);
658
+
659
+ const handleDelete = useCallback(async (id: string) => {
660
+ await deleteTodo(id);
661
+ await fetchTodos();
662
+ }, [deleteTodo, fetchTodos]);
663
+
664
+ if (loading && todos.length === 0) {
665
+ return <LoadingSpinner />;
666
+ }
667
+
668
+ if (error) {
669
+ return <ErrorMessage message={error.message} onRetry={fetchTodos} />;
670
+ }
671
+
672
+ if (todos.length === 0) {
673
+ return <EmptyState message="No todos yet. Create one!" />;
674
+ }
675
+
676
+ return (
677
+ <View style={styles.container}>
678
+ <TodoList
679
+ todos={todos}
680
+ onToggle={handleToggle}
681
+ onDelete={handleDelete}
682
+ refreshControl={
683
+ <RefreshControl refreshing={loading} onRefresh={fetchTodos} />
684
+ }
685
+ />
686
+ </View>
687
+ );
688
+ };
689
+
690
+ const styles = StyleSheet.create({
691
+ container: {
692
+ flex: 1,
693
+ },
694
+ });
695
+ ```
696
+
697
+ ### 12. Dumb Components (Presentation)
698
+
699
+ ```typescript
700
+ // presentation/components/dumb/todo-list.component.tsx
701
+
702
+ import React, { memo, ReactElement } from 'react';
703
+ import { FlatList, StyleSheet, RefreshControl } from 'react-native';
704
+ import { TodoDto } from '@/application/dtos/todo.dto';
705
+ import { TodoItem } from './todo-item.component';
706
+
707
+ interface TodoListProps {
708
+ todos: TodoDto[];
709
+ onToggle: (id: string) => void;
710
+ onDelete: (id: string) => void;
711
+ refreshControl?: ReactElement<typeof RefreshControl>;
712
+ }
713
+
714
+ export const TodoList: React.FC<TodoListProps> = memo(({
715
+ todos,
716
+ onToggle,
717
+ onDelete,
718
+ refreshControl
719
+ }) => {
720
+ return (
721
+ <FlatList
722
+ data={todos}
723
+ keyExtractor={(item) => item.id}
724
+ renderItem={({ item }) => (
725
+ <TodoItem
726
+ todo={item}
727
+ onToggle={onToggle}
728
+ onDelete={onDelete}
729
+ />
730
+ )}
731
+ refreshControl={refreshControl}
732
+ contentContainerStyle={styles.content}
733
+ />
734
+ );
735
+ });
736
+
737
+ TodoList.displayName = 'TodoList';
738
+
739
+ const styles = StyleSheet.create({
740
+ content: {
741
+ padding: 16,
742
+ },
743
+ });
744
+ ```
745
+
746
+ ```typescript
747
+ // presentation/components/dumb/todo-item.component.tsx
748
+
749
+ import React, { memo, useCallback } from 'react';
750
+ import { View, Text, Pressable, StyleSheet } from 'react-native';
751
+ import { TodoDto } from '@/application/dtos/todo.dto';
752
+
753
+ interface TodoItemProps {
754
+ todo: TodoDto;
755
+ onToggle: (id: string) => void;
756
+ onDelete: (id: string) => void;
757
+ }
758
+
759
+ export const TodoItem: React.FC<TodoItemProps> = memo(({ todo, onToggle, onDelete }) => {
760
+ const handleToggle = useCallback(() => {
761
+ onToggle(todo.id);
762
+ }, [onToggle, todo.id]);
763
+
764
+ const handleDelete = useCallback(() => {
765
+ onDelete(todo.id);
766
+ }, [onDelete, todo.id]);
767
+
768
+ return (
769
+ <View style={styles.container}>
770
+ <Pressable
771
+ onPress={handleToggle}
772
+ style={styles.checkbox}
773
+ accessibilityRole="checkbox"
774
+ accessibilityState={{ checked: todo.completed }}
775
+ accessibilityLabel={`Mark ${todo.title} as ${todo.completed ? 'incomplete' : 'complete'}`}
776
+ >
777
+ <View style={[styles.checkboxInner, todo.completed && styles.checked]} />
778
+ </Pressable>
779
+
780
+ <Text style={[styles.title, todo.completed && styles.completedTitle]}>
781
+ {todo.title}
782
+ </Text>
783
+
784
+ <Pressable
785
+ onPress={handleDelete}
786
+ style={styles.deleteButton}
787
+ accessibilityRole="button"
788
+ accessibilityLabel={`Delete ${todo.title}`}
789
+ >
790
+ <Text style={styles.deleteText}>x</Text>
791
+ </Pressable>
792
+ </View>
793
+ );
794
+ });
795
+
796
+ TodoItem.displayName = 'TodoItem';
797
+
798
+ const styles = StyleSheet.create({
799
+ container: {
800
+ flexDirection: 'row',
801
+ alignItems: 'center',
802
+ padding: 16,
803
+ backgroundColor: '#fff',
804
+ borderRadius: 8,
805
+ marginBottom: 8,
806
+ shadowColor: '#000',
807
+ shadowOffset: { width: 0, height: 1 },
808
+ shadowOpacity: 0.1,
809
+ shadowRadius: 2,
810
+ elevation: 2,
811
+ },
812
+ checkbox: {
813
+ width: 24,
814
+ height: 24,
815
+ borderRadius: 12,
816
+ borderWidth: 2,
817
+ borderColor: '#007AFF',
818
+ justifyContent: 'center',
819
+ alignItems: 'center',
820
+ marginRight: 12,
821
+ },
822
+ checkboxInner: {
823
+ width: 12,
824
+ height: 12,
825
+ borderRadius: 6,
826
+ },
827
+ checked: {
828
+ backgroundColor: '#007AFF',
829
+ },
830
+ title: {
831
+ flex: 1,
832
+ fontSize: 16,
833
+ color: '#333',
834
+ },
835
+ completedTitle: {
836
+ textDecorationLine: 'line-through',
837
+ color: '#999',
838
+ },
839
+ deleteButton: {
840
+ padding: 8,
841
+ },
842
+ deleteText: {
843
+ fontSize: 24,
844
+ color: '#FF3B30',
845
+ fontWeight: 'bold',
846
+ },
847
+ });
848
+ ```
849
+
850
+ ```typescript
851
+ // presentation/components/dumb/todo-item.component.test.tsx
852
+
853
+ import React from 'react';
854
+ import { render, fireEvent } from '@testing-library/react-native';
855
+ import { TodoItem } from './todo-item.component';
856
+ import { TodoDto } from '@/application/dtos/todo.dto';
857
+
858
+ describe('TodoItem', () => {
859
+ const mockTodo: TodoDto = {
860
+ id: '1',
861
+ title: 'Test Todo',
862
+ completed: false,
863
+ createdAt: new Date().toISOString(),
864
+ };
865
+
866
+ it('should render todo title', () => {
867
+ const { getByText } = render(
868
+ <TodoItem todo={mockTodo} onToggle={jest.fn()} onDelete={jest.fn()} />
869
+ );
870
+
871
+ expect(getByText('Test Todo')).toBeTruthy();
872
+ });
873
+
874
+ it('should call onToggle when checkbox pressed', () => {
875
+ const onToggle = jest.fn();
876
+ const { getByRole } = render(
877
+ <TodoItem todo={mockTodo} onToggle={onToggle} onDelete={jest.fn()} />
878
+ );
879
+
880
+ fireEvent.press(getByRole('checkbox'));
881
+ expect(onToggle).toHaveBeenCalledWith('1');
882
+ });
883
+
884
+ it('should call onDelete when delete button pressed', () => {
885
+ const onDelete = jest.fn();
886
+ const { getByLabelText } = render(
887
+ <TodoItem todo={mockTodo} onToggle={jest.fn()} onDelete={onDelete} />
888
+ );
889
+
890
+ fireEvent.press(getByLabelText('Delete Test Todo'));
891
+ expect(onDelete).toHaveBeenCalledWith('1');
892
+ });
893
+
894
+ it('should apply completed styles when todo is completed', () => {
895
+ const completedTodo = { ...mockTodo, completed: true };
896
+ const { getByText } = render(
897
+ <TodoItem todo={completedTodo} onToggle={jest.fn()} onDelete={jest.fn()} />
898
+ );
899
+
900
+ const titleElement = getByText('Test Todo');
901
+ expect(titleElement.props.style).toContainEqual(
902
+ expect.objectContaining({ textDecorationLine: 'line-through' })
903
+ );
904
+ });
905
+ });
906
+ ```
907
+
908
+ ### 13. Expo Router Layout
909
+
910
+ ```typescript
911
+ // app/_layout.tsx
912
+
913
+ import { Stack } from 'expo-router';
914
+ import { StatusBar } from 'expo-status-bar';
915
+ import { ContainerProvider } from '@/infrastructure/di/container';
916
+
917
+ export default function RootLayout() {
918
+ return (
919
+ <ContainerProvider>
920
+ <StatusBar style="auto" />
921
+ <Stack
922
+ screenOptions={{
923
+ headerStyle: { backgroundColor: '#007AFF' },
924
+ headerTintColor: '#fff',
925
+ headerTitleStyle: { fontWeight: 'bold' },
926
+ }}
927
+ />
928
+ </ContainerProvider>
929
+ );
930
+ }
931
+ ```
932
+
933
+ ```typescript
934
+ // app/index.tsx
935
+
936
+ import { SafeAreaView, StyleSheet } from 'react-native';
937
+ import { Stack } from 'expo-router';
938
+ import { TodoListContainer } from '@/presentation/components/smart/todo-list.container';
939
+
940
+ export default function HomeScreen() {
941
+ return (
942
+ <SafeAreaView style={styles.container}>
943
+ <Stack.Screen options={{ title: 'My Todos' }} />
944
+ <TodoListContainer />
945
+ </SafeAreaView>
946
+ );
947
+ }
948
+
949
+ const styles = StyleSheet.create({
950
+ container: {
951
+ flex: 1,
952
+ backgroundColor: '#f5f5f5',
953
+ },
954
+ });
955
+ ```
956
+
957
+ ## Checklist Clean Hexa Mobile
958
+
959
+ Avant de valider ton code, vérifie :
960
+
961
+ ### Domain
962
+ - [ ] Aucun import React/Expo dans domain/
963
+ - [ ] Entities immuables avec méthodes qui retournent de nouvelles instances
964
+ - [ ] Validation dans les factories (static create)
965
+ - [ ] Ports = interfaces TypeScript pures
966
+
967
+ ### Application
968
+ - [ ] Use cases = custom hooks
969
+ - [ ] Dépendances injectées en paramètre du hook
970
+ - [ ] États: loading, error, data, success
971
+ - [ ] Tests avec mocks des ports
972
+
973
+ ### Infrastructure
974
+ - [ ] Adapters implémentent les ports
975
+ - [ ] Container avec React Context
976
+ - [ ] Pas de singletons exportés directement
977
+ - [ ] Tests des adapters
978
+
979
+ ### Presentation
980
+ - [ ] Smart components connectent use cases aux dumb
981
+ - [ ] Dumb components = props only + memo
982
+ - [ ] FlatList pour les listes (pas ScrollView)
983
+ - [ ] Accessibilité (accessibilityLabel, accessibilityRole)
984
+ - [ ] Tests des dumb components