ai-flow-dev 2.6.0 → 2.8.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.
- package/README.md +24 -21
- package/package.json +6 -6
- package/prompts/backend/flow-check-review.md +648 -12
- package/prompts/backend/flow-check-test.md +520 -8
- package/prompts/backend/flow-check.md +687 -29
- package/prompts/backend/flow-commit.md +18 -49
- package/prompts/backend/flow-finish.md +919 -0
- package/prompts/backend/flow-release.md +949 -0
- package/prompts/backend/flow-work.md +296 -221
- package/prompts/desktop/flow-check-review.md +648 -12
- package/prompts/desktop/flow-check-test.md +520 -8
- package/prompts/desktop/flow-check.md +687 -29
- package/prompts/desktop/flow-commit.md +18 -49
- package/prompts/desktop/flow-finish.md +910 -0
- package/prompts/desktop/flow-release.md +662 -0
- package/prompts/desktop/flow-work.md +398 -219
- package/prompts/frontend/flow-check-review.md +648 -12
- package/prompts/frontend/flow-check-test.md +520 -8
- package/prompts/frontend/flow-check.md +687 -29
- package/prompts/frontend/flow-commit.md +18 -49
- package/prompts/frontend/flow-finish.md +910 -0
- package/prompts/frontend/flow-release.md +519 -0
- package/prompts/frontend/flow-work-api.md +1540 -0
- package/prompts/frontend/flow-work.md +774 -218
- package/prompts/mobile/flow-check-review.md +648 -12
- package/prompts/mobile/flow-check-test.md +520 -8
- package/prompts/mobile/flow-check.md +687 -29
- package/prompts/mobile/flow-commit.md +18 -49
- package/prompts/mobile/flow-finish.md +910 -0
- package/prompts/mobile/flow-release.md +751 -0
- package/prompts/mobile/flow-work-api.md +1493 -0
- package/prompts/mobile/flow-work.md +792 -222
- package/templates/AGENT.template.md +1 -1
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Analyze OpenAPI specification to extract complete module metadata for mobile CRUD generation (React Native)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# API Module Analyzer (Sub-Prompt - Mobile)
|
|
6
|
+
|
|
7
|
+
**YOU ARE AN EXPERT API ANALYZER specialized in extracting comprehensive metadata from OpenAPI specifications for React Native mobile code generation.**
|
|
8
|
+
|
|
9
|
+
## ⚠️ IMPORTANT: Internal Sub-Prompt
|
|
10
|
+
|
|
11
|
+
**DO NOT invoke this prompt directly.** This is an internal sub-prompt called by `/flow-work`.
|
|
12
|
+
|
|
13
|
+
**To use API Module Analysis (mobile), run:**
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
/flow-work api <module-name>
|
|
17
|
+
# Example: /flow-work api users
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Why not call directly?**
|
|
21
|
+
|
|
22
|
+
- `/flow-work` manages URL cache (`.ai-flow/cache/api-config.json`)
|
|
23
|
+
- `/flow-work` handles connection errors with interactive retry
|
|
24
|
+
- `/flow-work` validates URL before analysis
|
|
25
|
+
- This sub-prompt expects a **pre-validated URL** as input
|
|
26
|
+
|
|
27
|
+
**Architecture:**
|
|
28
|
+
|
|
29
|
+
- `flow-work` = Orchestrator (stateful, manages cache)
|
|
30
|
+
- `flow-work-api` = Analyzer (stateless, pure function)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Invocation Context
|
|
35
|
+
|
|
36
|
+
This sub-prompt is automatically invoked by `/flow-work` when the pattern `api [module-name]` is detected.
|
|
37
|
+
|
|
38
|
+
## Purpose
|
|
39
|
+
|
|
40
|
+
Parse OpenAPI backend specification and return structured analysis data that `flow-work` will use to:
|
|
41
|
+
|
|
42
|
+
1. Generate detailed `work.md` with field specifications
|
|
43
|
+
2. Execute CRUD implementation with mobile-specific patterns (React Native)
|
|
44
|
+
3. Ensure type-safety between mobile app and backend
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Input Parameters
|
|
49
|
+
|
|
50
|
+
Received from parent prompt (flow-work):
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
interface ApiModuleInput {
|
|
54
|
+
module: string; // 'users', 'organizations', 'audit-logs'
|
|
55
|
+
apiUrl?: string; // Override default OpenAPI endpoint
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Default API URL**: `http://localhost:3001/api/docs-json`
|
|
60
|
+
**Override**: User can specify `--api-url=http://other-host:3000/api/docs`
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Phase 0: API Analysis (Automatic)
|
|
65
|
+
|
|
66
|
+
### 0.1. Fetch OpenAPI Specification (Robust)
|
|
67
|
+
|
|
68
|
+
**CRITICAL: Handle connection errors, timeouts, and network issues.**
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
async function fetchOpenAPISpec(url: string): Promise<OpenAPISpec> {
|
|
72
|
+
try {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
|
|
75
|
+
|
|
76
|
+
const response = await fetch(url, {
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
headers: { Accept: 'application/json' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const spec = await response.json();
|
|
88
|
+
|
|
89
|
+
// Validate OpenAPI version
|
|
90
|
+
if (!spec.openapi && !spec.swagger) {
|
|
91
|
+
throw new Error('Invalid OpenAPI/Swagger specification');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return spec;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error.name === 'AbortError') {
|
|
97
|
+
throw new Error('API documentation server timeout. Ensure backend is running.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (error.message.includes('Network request failed')) {
|
|
101
|
+
throw new Error('Network error. Check device connectivity or backend URL.');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**IF fetch fails:**
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
❌ Failed to fetch OpenAPI spec from http://localhost:3001/api/docs-json
|
|
113
|
+
|
|
114
|
+
Error: API documentation server timeout. Ensure backend is running.
|
|
115
|
+
|
|
116
|
+
Options:
|
|
117
|
+
A) Retry with different URL
|
|
118
|
+
B) Use cached spec (if available)
|
|
119
|
+
C) Proceed with manual type definitions
|
|
120
|
+
D) Cancel
|
|
121
|
+
|
|
122
|
+
Your choice: _
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**IF successful, show spec version:**
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
✅ OpenAPI Specification Loaded
|
|
129
|
+
|
|
130
|
+
Version: OpenAPI 3.0.3
|
|
131
|
+
Title: CROSS Mobile API
|
|
132
|
+
Paths: 45 endpoints detected
|
|
133
|
+
Schemas: 32 types available
|
|
134
|
+
|
|
135
|
+
Proceeding with analysis...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 0.2. Extract Module Endpoints (Filtered)
|
|
139
|
+
|
|
140
|
+
**CRITICAL: Extract ONLY the target module endpoints. Do NOT extract all API modules.**
|
|
141
|
+
|
|
142
|
+
Identify all endpoints for the specified module:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Example for "users" module
|
|
146
|
+
const targetModule = 'users'; // From user input
|
|
147
|
+
|
|
148
|
+
const moduleEndpoints = filterEndpoints(spec.paths, {
|
|
149
|
+
tags: [capitalizeFirst(targetModule)], // ['Users']
|
|
150
|
+
pathPrefix: `/api/${targetModule}`, // '/api/users'
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Result:
|
|
154
|
+
{
|
|
155
|
+
list: 'GET /api/users',
|
|
156
|
+
create: 'POST /api/users',
|
|
157
|
+
get: 'GET /api/users/{id}',
|
|
158
|
+
update: 'PUT /api/users/{id}',
|
|
159
|
+
delete: 'DELETE /api/users/{id}',
|
|
160
|
+
// Additional endpoints:
|
|
161
|
+
getMe: 'GET /api/users/me',
|
|
162
|
+
updateMe: 'PUT /api/users/me',
|
|
163
|
+
changePassword: 'PUT /api/users/me/password',
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**⚠️ IMPORTANT**: Do NOT include endpoints from other modules like `/api/organizations`, `/api/audit-logs`, etc. Only the target module.
|
|
168
|
+
|
|
169
|
+
### 0.3. Detect Pagination Response Format
|
|
170
|
+
|
|
171
|
+
**Analyze the response schema for list endpoints:**
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
function detectPaginationFormat(endpoint: OpenAPIEndpoint): PaginationFormat {
|
|
175
|
+
const responseSchema = endpoint.responses['200'].schema;
|
|
176
|
+
|
|
177
|
+
// Check if response is object with data/items property
|
|
178
|
+
if (responseSchema.type === 'object') {
|
|
179
|
+
if (responseSchema.properties?.items && responseSchema.properties?.total) {
|
|
180
|
+
return {
|
|
181
|
+
type: 'object',
|
|
182
|
+
dataKey: 'items',
|
|
183
|
+
totalKey: 'total',
|
|
184
|
+
pageKey: responseSchema.properties.page ? 'page' : null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (responseSchema.properties?.data && responseSchema.properties?.meta) {
|
|
189
|
+
return {
|
|
190
|
+
type: 'object',
|
|
191
|
+
dataKey: 'data',
|
|
192
|
+
totalKey: 'meta.total',
|
|
193
|
+
pageKey: 'meta.page',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Response is array directly
|
|
199
|
+
if (responseSchema.type === 'array') {
|
|
200
|
+
return {
|
|
201
|
+
type: 'array',
|
|
202
|
+
dataKey: null,
|
|
203
|
+
totalKey: null, // Client-side pagination only
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
throw new Error('Unable to detect pagination format from OpenAPI schema');
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 0.4. Extract Complete Field Specifications (Detailed)
|
|
212
|
+
|
|
213
|
+
**For EACH endpoint, extract FULL field specifications:**
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
interface FieldSpec {
|
|
217
|
+
name: string;
|
|
218
|
+
type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object';
|
|
219
|
+
required: boolean;
|
|
220
|
+
nullable: boolean;
|
|
221
|
+
validation?: {
|
|
222
|
+
min?: number;
|
|
223
|
+
max?: number;
|
|
224
|
+
pattern?: string;
|
|
225
|
+
format?: 'email' | 'url' | 'uuid' | 'date-time';
|
|
226
|
+
enum?: string[];
|
|
227
|
+
};
|
|
228
|
+
relation?: {
|
|
229
|
+
entity: string;
|
|
230
|
+
type: 'one-to-one' | 'many-to-one' | 'one-to-many' | 'many-to-many';
|
|
231
|
+
populated: boolean; // Si el backend devuelve el objeto completo
|
|
232
|
+
displayField: string; // Campo a mostrar (e.g., "name", "email")
|
|
233
|
+
};
|
|
234
|
+
default?: any;
|
|
235
|
+
description?: string;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Extract from OpenAPI schema:**
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// Example extraction
|
|
243
|
+
const userSchema = spec.components.schemas.UserResponseDto;
|
|
244
|
+
|
|
245
|
+
const fields: FieldSpec[] = Object.entries(userSchema.properties).map(([name, prop]) => ({
|
|
246
|
+
name,
|
|
247
|
+
type: mapOpenAPIType(prop.type, prop.format),
|
|
248
|
+
required: userSchema.required?.includes(name) ?? false,
|
|
249
|
+
nullable: prop.nullable ?? false,
|
|
250
|
+
validation: extractValidation(prop),
|
|
251
|
+
relation: detectRelation(name, prop, spec),
|
|
252
|
+
default: prop.default,
|
|
253
|
+
description: prop.description,
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
// Helper: Detect relations
|
|
257
|
+
function detectRelation(fieldName: string, prop: any, spec: OpenAPISpec) {
|
|
258
|
+
// Pattern 1: Field ends with "Id" → Foreign Key
|
|
259
|
+
if (fieldName.endsWith('Id') && prop.type === 'string') {
|
|
260
|
+
const entityName = fieldName.slice(0, -2); // "roleId" → "role"
|
|
261
|
+
return {
|
|
262
|
+
entity: entityName,
|
|
263
|
+
type: 'many-to-one',
|
|
264
|
+
populated: false,
|
|
265
|
+
displayField: 'name', // Default assumption
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Pattern 2: Field is object with $ref → Populated relation
|
|
270
|
+
if (prop.$ref) {
|
|
271
|
+
const refSchema = resolveRef(prop.$ref, spec);
|
|
272
|
+
return {
|
|
273
|
+
entity: extractEntityName(prop.$ref),
|
|
274
|
+
type: 'many-to-one',
|
|
275
|
+
populated: true,
|
|
276
|
+
displayField: detectDisplayField(refSchema), // Smart detection
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Pattern 3: Array of objects → One-to-many or Many-to-many
|
|
281
|
+
if (prop.type === 'array' && prop.items?.$ref) {
|
|
282
|
+
return {
|
|
283
|
+
entity: extractEntityName(prop.items.$ref),
|
|
284
|
+
type: 'one-to-many', // or 'many-to-many' based on naming
|
|
285
|
+
populated: true,
|
|
286
|
+
displayField: 'name',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Helper: Detect which field to display (smart heuristic)
|
|
294
|
+
function detectDisplayField(schema: any): string {
|
|
295
|
+
const commonDisplayFields = ['name', 'title', 'label', 'email', 'username'];
|
|
296
|
+
for (const field of commonDisplayFields) {
|
|
297
|
+
if (schema.properties?.[field]) return field;
|
|
298
|
+
}
|
|
299
|
+
return 'id'; // Fallback
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 0.5. Categorize Fields by Usage
|
|
304
|
+
|
|
305
|
+
**Auto-categorize fields based on patterns:**
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
function categorizeField(field: FieldSpec, dto: 'response' | 'create' | 'update'): FieldCategory {
|
|
309
|
+
const autoGeneratedPatterns = ['id', 'createdAt', 'updatedAt', 'deletedAt', 'version'];
|
|
310
|
+
const readOnlyPatterns = ['lastLoginAt', 'emailVerifiedAt', '_count', 'computed'];
|
|
311
|
+
const metadataPatterns = ['metadata', 'config', 'settings'];
|
|
312
|
+
|
|
313
|
+
// Auto-generated (never editable)
|
|
314
|
+
if (autoGeneratedPatterns.includes(field.name)) {
|
|
315
|
+
return {
|
|
316
|
+
category: 'auto-generated',
|
|
317
|
+
showInList: field.name === 'id' ? false : true, // Mobile uses "List" not "Table"
|
|
318
|
+
showInForm: false,
|
|
319
|
+
showInDetails: true,
|
|
320
|
+
editable: false,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Read-only (show but not editable)
|
|
325
|
+
if (readOnlyPatterns.some((p) => field.name.includes(p))) {
|
|
326
|
+
return {
|
|
327
|
+
category: 'read-only',
|
|
328
|
+
showInList: true,
|
|
329
|
+
showInForm: false,
|
|
330
|
+
showInDetails: true,
|
|
331
|
+
editable: false,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Metadata (advanced users only)
|
|
336
|
+
if (metadataPatterns.includes(field.name)) {
|
|
337
|
+
return {
|
|
338
|
+
category: 'metadata',
|
|
339
|
+
showInList: false,
|
|
340
|
+
showInForm: false, // Or in advanced section
|
|
341
|
+
showInDetails: true,
|
|
342
|
+
editable: true,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Editable (normal fields)
|
|
347
|
+
return {
|
|
348
|
+
category: 'editable',
|
|
349
|
+
showInList: true,
|
|
350
|
+
showInForm: dto === 'response' ? false : true,
|
|
351
|
+
showInDetails: true,
|
|
352
|
+
editable: dto === 'update' ? true : false,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 0.6. Extract DTOs and Schemas
|
|
358
|
+
|
|
359
|
+
Analyze `components.schemas` to extract:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
interface ModuleSchemas {
|
|
363
|
+
response: Schema; // UserResponseDto
|
|
364
|
+
create: Schema; // CreateUserDto
|
|
365
|
+
update: Schema; // UpdateUserDto
|
|
366
|
+
filters: QueryParams; // page, limit, search, etc.
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 0.7. Detect Features
|
|
371
|
+
|
|
372
|
+
Auto-detect capabilities:
|
|
373
|
+
|
|
374
|
+
```yaml
|
|
375
|
+
Features_Detected:
|
|
376
|
+
pagination:
|
|
377
|
+
enabled: true/false
|
|
378
|
+
params: [page, limit]
|
|
379
|
+
response_format: "array" | "{ data, meta }"
|
|
380
|
+
|
|
381
|
+
search:
|
|
382
|
+
enabled: true/false
|
|
383
|
+
params: [search, filter_field_1, filter_field_2]
|
|
384
|
+
|
|
385
|
+
sorting:
|
|
386
|
+
enabled: true/false
|
|
387
|
+
params: [sortBy, order]
|
|
388
|
+
|
|
389
|
+
authentication:
|
|
390
|
+
type: bearer
|
|
391
|
+
required: true
|
|
392
|
+
|
|
393
|
+
authorization:
|
|
394
|
+
roles: [ROOT, OWNER, ADMIN]
|
|
395
|
+
permissions: ['user:read', 'user:write']
|
|
396
|
+
|
|
397
|
+
soft_delete:
|
|
398
|
+
enabled: true/false
|
|
399
|
+
status_field: "status" | "deletedAt"
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### 0.8. Analyze Relationships (Smart Depth)
|
|
403
|
+
|
|
404
|
+
**Extract ONLY direct relationships (depth 1) to avoid analyzing unnecessary modules.**
|
|
405
|
+
|
|
406
|
+
Detect foreign keys and relations:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
function extractRelevantRelationships(
|
|
410
|
+
targetModule: string,
|
|
411
|
+
spec: OpenAPISpec,
|
|
412
|
+
maxDepth: number = 1
|
|
413
|
+
): Relationships[] {
|
|
414
|
+
const targetSchemas = getModuleSchemas(spec, targetModule);
|
|
415
|
+
const relationships: Relationships[] = [];
|
|
416
|
+
|
|
417
|
+
// Only analyze target module schemas
|
|
418
|
+
targetSchemas.forEach(schema => {
|
|
419
|
+
Object.entries(schema.properties).forEach(([fieldName, prop]) => {
|
|
420
|
+
// Detect foreign key (e.g., roleId)
|
|
421
|
+
if (fieldName.endsWith('Id') && prop.type === 'string') {
|
|
422
|
+
const relatedEntity = fieldName.slice(0, -2); // 'roleId' → 'role'
|
|
423
|
+
|
|
424
|
+
relationships.push({
|
|
425
|
+
field: fieldName,
|
|
426
|
+
relatedEntity: capitalizeFirst(relatedEntity),
|
|
427
|
+
type: 'many-to-one',
|
|
428
|
+
foreignKey: fieldName,
|
|
429
|
+
endpoint: `/api/${relatedEntity}s`, // Pluralize
|
|
430
|
+
displayField: 'name', // Default assumption
|
|
431
|
+
populated: false,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Detect populated relation (e.g., role: { $ref: '#/components/schemas/Role' })
|
|
436
|
+
if (prop.$ref) {
|
|
437
|
+
const relatedEntity = extractEntityName(prop.$ref);
|
|
438
|
+
|
|
439
|
+
relationships.push({
|
|
440
|
+
field: fieldName,
|
|
441
|
+
relatedEntity,
|
|
442
|
+
type: 'many-to-one',
|
|
443
|
+
foreignKey: `${fieldName}Id`,
|
|
444
|
+
endpoint: `/api/${fieldName}s`,
|
|
445
|
+
displayField: detectDisplayField(resolveRef(prop.$ref, spec)),
|
|
446
|
+
populated: true,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Detect array relations (e.g., organizations: [{ $ref: '...' }])
|
|
451
|
+
if (prop.type === 'array' && prop.items?.$ref) {
|
|
452
|
+
const relatedEntity = extractEntityName(prop.items.$ref);
|
|
453
|
+
|
|
454
|
+
relationships.push({
|
|
455
|
+
field: fieldName,
|
|
456
|
+
relatedEntity,
|
|
457
|
+
type: 'one-to-many', // or 'many-to-many'
|
|
458
|
+
foreignKey: `${targetModule}Id`,
|
|
459
|
+
endpoint: `/api/${fieldName}`,
|
|
460
|
+
displayField: 'name',
|
|
461
|
+
populated: true,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
return relationships;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Result for "users" module:
|
|
471
|
+
{
|
|
472
|
+
role: {
|
|
473
|
+
field: 'roleId',
|
|
474
|
+
relatedEntity: 'Role',
|
|
475
|
+
type: "many-to-one",
|
|
476
|
+
foreignKey: "roleId",
|
|
477
|
+
endpoint: "/api/roles",
|
|
478
|
+
displayField: "name",
|
|
479
|
+
populated: false,
|
|
480
|
+
},
|
|
481
|
+
organization: {
|
|
482
|
+
field: 'organizationId',
|
|
483
|
+
relatedEntity: 'Organization',
|
|
484
|
+
type: "many-to-one",
|
|
485
|
+
foreignKey: "organizationId",
|
|
486
|
+
endpoint: "/api/organizations",
|
|
487
|
+
displayField: "name",
|
|
488
|
+
populated: false,
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**✅ Only extract schemas for related entities (Role, Organization)**
|
|
494
|
+
**❌ Do NOT extract full CRUD endpoints for related modules**
|
|
495
|
+
**❌ Do NOT analyze deep nested relations (maxDepth = 1)**
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Phase 0.9: Current Implementation Audit (CRITICAL)
|
|
500
|
+
|
|
501
|
+
**Compare OpenAPI specification with existing mobile code to detect gaps and errors.**
|
|
502
|
+
|
|
503
|
+
### Step 1: Check if Feature Exists
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
const featurePath = `src/features/${targetModule}`;
|
|
507
|
+
const featureExists = await fileExists(featurePath);
|
|
508
|
+
|
|
509
|
+
if (!featureExists) {
|
|
510
|
+
return {
|
|
511
|
+
status: 'NOT_IMPLEMENTED',
|
|
512
|
+
action: 'FULL_IMPLEMENTATION',
|
|
513
|
+
message: `Feature directory does not exist. Full implementation required.`,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Step 2: Scan Existing Files
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
const existingFiles = {
|
|
522
|
+
types: await glob(`${featurePath}/types/**/*.ts`),
|
|
523
|
+
schemas: await glob(`${featurePath}/schemas/**/*.ts`),
|
|
524
|
+
services: await glob(`${featurePath}/services/**/*.ts`),
|
|
525
|
+
hooks: await glob(`${featurePath}/hooks/**/*.ts`),
|
|
526
|
+
components: await glob(`${featurePath}/components/**/*.tsx`),
|
|
527
|
+
screens: await glob(`${featurePath}/screens/**/*.tsx`), // Mobile uses "screens" not "pages"
|
|
528
|
+
tests: await glob(`${featurePath}/**/*.test.{ts,tsx}`),
|
|
529
|
+
};
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Step 3: Compare Types with OpenAPI Schemas
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
async function auditTypes(
|
|
536
|
+
openapiSchemas: Schema[],
|
|
537
|
+
existingTypeFiles: string[]
|
|
538
|
+
): Promise<TypeAuditResult> {
|
|
539
|
+
const audit = {
|
|
540
|
+
matching: [],
|
|
541
|
+
missing: [],
|
|
542
|
+
incorrect: [],
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
for (const schema of openapiSchemas) {
|
|
546
|
+
const typeFile = existingTypeFiles.find(
|
|
547
|
+
(f) => f.includes(schema.name) || f.includes('entities.ts')
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
if (!typeFile) {
|
|
551
|
+
audit.missing.push({
|
|
552
|
+
schema: schema.name,
|
|
553
|
+
action: 'CREATE',
|
|
554
|
+
reason: 'Type definition not found',
|
|
555
|
+
});
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Parse existing type
|
|
560
|
+
const fileContent = await readFile(typeFile);
|
|
561
|
+
const existingType = parseTypeScriptInterface(fileContent, schema.name);
|
|
562
|
+
|
|
563
|
+
if (!existingType) {
|
|
564
|
+
audit.missing.push({
|
|
565
|
+
schema: schema.name,
|
|
566
|
+
action: 'CREATE',
|
|
567
|
+
reason: `Type not found in ${typeFile}`,
|
|
568
|
+
});
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Compare fields
|
|
573
|
+
const fieldComparison = compareFields(existingType.fields, schema.properties);
|
|
574
|
+
|
|
575
|
+
if (fieldComparison.hasDifferences) {
|
|
576
|
+
audit.incorrect.push({
|
|
577
|
+
schema: schema.name,
|
|
578
|
+
file: typeFile,
|
|
579
|
+
issues: [
|
|
580
|
+
...fieldComparison.missingFields.map((f) => `Missing field: ${f}`),
|
|
581
|
+
...fieldComparison.extraFields.map((f) => `Extra field: ${f}`),
|
|
582
|
+
...fieldComparison.typeMismatches.map(
|
|
583
|
+
(m) => `Type mismatch for ${m.field}: expected ${m.expected}, got ${m.actual}`
|
|
584
|
+
),
|
|
585
|
+
],
|
|
586
|
+
action: 'UPDATE',
|
|
587
|
+
});
|
|
588
|
+
} else {
|
|
589
|
+
audit.matching.push(schema.name);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return audit;
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Step 4: Compare Endpoints with Hooks
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
async function auditHooks(
|
|
601
|
+
openapiEndpoints: Endpoint[],
|
|
602
|
+
existingHookFiles: string[]
|
|
603
|
+
): Promise<HookAuditResult> {
|
|
604
|
+
const audit = {
|
|
605
|
+
implemented: [],
|
|
606
|
+
missing: [],
|
|
607
|
+
incorrect: [],
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// Expected hooks based on endpoints
|
|
611
|
+
const expectedHooks = {
|
|
612
|
+
list: openapiEndpoints.find((e) => e.method === 'GET' && !e.path.includes('{')),
|
|
613
|
+
get: openapiEndpoints.find((e) => e.method === 'GET' && e.path.includes('{id}')),
|
|
614
|
+
create: openapiEndpoints.find((e) => e.method === 'POST'),
|
|
615
|
+
update: openapiEndpoints.find((e) => e.method === 'PUT'),
|
|
616
|
+
delete: openapiEndpoints.find((e) => e.method === 'DELETE'),
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
for (const [hookType, endpoint] of Object.entries(expectedHooks)) {
|
|
620
|
+
if (!endpoint) continue;
|
|
621
|
+
|
|
622
|
+
const expectedHookName = `use${capitalizeFirst(targetModule)}${capitalizeFirst(hookType === 'list' ? '' : hookType)}`;
|
|
623
|
+
const hookFile = existingHookFiles.find((f) => f.includes(expectedHookName));
|
|
624
|
+
|
|
625
|
+
if (!hookFile) {
|
|
626
|
+
audit.missing.push({
|
|
627
|
+
endpoint: `${endpoint.method} ${endpoint.path}`,
|
|
628
|
+
expectedHook: expectedHookName,
|
|
629
|
+
action: 'CREATE',
|
|
630
|
+
});
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Analyze hook implementation
|
|
635
|
+
const hookContent = await readFile(hookFile);
|
|
636
|
+
const hookAnalysis = analyzeHookCode(hookContent);
|
|
637
|
+
|
|
638
|
+
const issues = [];
|
|
639
|
+
|
|
640
|
+
if (!hookAnalysis.hasQueryKey) {
|
|
641
|
+
issues.push('Missing query key constant');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!hookAnalysis.isTypeSafe) {
|
|
645
|
+
issues.push('Uses `any` type or missing type annotations');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (!hookAnalysis.hasErrorHandling) {
|
|
649
|
+
issues.push('Missing onError handler');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (
|
|
653
|
+
hookAnalysis.hasCacheInvalidation === false &&
|
|
654
|
+
['create', 'update', 'delete'].includes(hookType)
|
|
655
|
+
) {
|
|
656
|
+
issues.push('Missing cache invalidation on success');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (issues.length > 0) {
|
|
660
|
+
audit.incorrect.push({
|
|
661
|
+
hook: expectedHookName,
|
|
662
|
+
file: hookFile,
|
|
663
|
+
issues,
|
|
664
|
+
action: 'FIX',
|
|
665
|
+
});
|
|
666
|
+
} else {
|
|
667
|
+
audit.implemented.push(expectedHookName);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return audit;
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Step 5: Audit UI Components (Mobile-Specific)
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
async function auditComponents(
|
|
679
|
+
targetModule: string,
|
|
680
|
+
existingComponents: string[]
|
|
681
|
+
): Promise<ComponentAuditResult> {
|
|
682
|
+
const expectedComponents = [
|
|
683
|
+
`${capitalizeFirst(targetModule)}List.tsx`, // FlatList component
|
|
684
|
+
`${capitalizeFirst(targetModule)}BottomSheet.tsx`, // Mobile uses Bottom Sheet instead of Drawer
|
|
685
|
+
`${capitalizeFirst(targetModule)}Form.tsx`,
|
|
686
|
+
`Delete${capitalizeFirst(targetModule)}Modal.tsx`, // Mobile uses Modal instead of Dialog
|
|
687
|
+
`${capitalizeFirst(targetModule)}Filters.tsx`,
|
|
688
|
+
];
|
|
689
|
+
|
|
690
|
+
const audit = {
|
|
691
|
+
complete: [],
|
|
692
|
+
missing: [],
|
|
693
|
+
nonStandard: [],
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
for (const expectedComponent of expectedComponents) {
|
|
697
|
+
const componentFile = existingComponents.find((c) => c.endsWith(expectedComponent));
|
|
698
|
+
|
|
699
|
+
if (!componentFile) {
|
|
700
|
+
audit.missing.push({
|
|
701
|
+
component: expectedComponent,
|
|
702
|
+
action: 'CREATE',
|
|
703
|
+
});
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Analyze component quality
|
|
708
|
+
const componentContent = await readFile(componentFile);
|
|
709
|
+
const componentAnalysis = analyzeComponentCode(componentContent);
|
|
710
|
+
|
|
711
|
+
const issues = [];
|
|
712
|
+
|
|
713
|
+
if (expectedComponent.includes('List') && !componentAnalysis.usesFlatList) {
|
|
714
|
+
issues.push('Should use FlatList (React Native standard)');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (expectedComponent.includes('Form') && !componentAnalysis.usesReactHookForm) {
|
|
718
|
+
issues.push('Should use React Hook Form (project standard)');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (expectedComponent.includes('Form') && !componentAnalysis.usesZodValidation) {
|
|
722
|
+
issues.push('Missing Zod validation');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (!componentAnalysis.hasLoadingStates) {
|
|
726
|
+
issues.push('Missing loading states (ActivityIndicator)');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (!componentAnalysis.hasErrorHandling) {
|
|
730
|
+
issues.push('Missing error handling');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (issues.length > 0) {
|
|
734
|
+
audit.nonStandard.push({
|
|
735
|
+
component: expectedComponent,
|
|
736
|
+
file: componentFile,
|
|
737
|
+
issues,
|
|
738
|
+
action: 'REFACTOR',
|
|
739
|
+
});
|
|
740
|
+
} else {
|
|
741
|
+
audit.complete.push(expectedComponent);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return audit;
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Step 6: Generate Implementation Status Report
|
|
750
|
+
|
|
751
|
+
```typescript
|
|
752
|
+
function generateStatusReport(audits: AllAudits): ImplementationStatusReport {
|
|
753
|
+
const totalItems =
|
|
754
|
+
audits.types.missing.length +
|
|
755
|
+
audits.types.matching.length +
|
|
756
|
+
audits.types.incorrect.length +
|
|
757
|
+
audits.hooks.missing.length +
|
|
758
|
+
audits.hooks.implemented.length +
|
|
759
|
+
audits.hooks.incorrect.length +
|
|
760
|
+
audits.components.missing.length +
|
|
761
|
+
audits.components.complete.length +
|
|
762
|
+
audits.components.nonStandard.length;
|
|
763
|
+
|
|
764
|
+
const completeItems =
|
|
765
|
+
audits.types.matching.length +
|
|
766
|
+
audits.hooks.implemented.length +
|
|
767
|
+
audits.components.complete.length;
|
|
768
|
+
|
|
769
|
+
const score = Math.round((completeItems / totalItems) * 100);
|
|
770
|
+
|
|
771
|
+
let strategy: ImplementationStrategy;
|
|
772
|
+
if (score < 30) {
|
|
773
|
+
strategy = 'FULL_NEW';
|
|
774
|
+
} else if (score < 70) {
|
|
775
|
+
strategy = 'REFACTOR_COMPLETE';
|
|
776
|
+
} else {
|
|
777
|
+
strategy = 'MINOR_FIXES';
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return {
|
|
781
|
+
score,
|
|
782
|
+
strategy,
|
|
783
|
+
audits,
|
|
784
|
+
summary: generateSummaryText(audits, score, strategy),
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
**Report Output (Mobile):**
|
|
790
|
+
|
|
791
|
+
```
|
|
792
|
+
📊 Current Implementation Status: Users Module (Mobile)
|
|
793
|
+
|
|
794
|
+
🗂️ Feature Structure:
|
|
795
|
+
✅ Directory exists: src/features/users/
|
|
796
|
+
|
|
797
|
+
📦 Types & Schemas:
|
|
798
|
+
✅ UserResponseDto (matching OpenAPI)
|
|
799
|
+
⚠️ CreateUserDto (missing fields: roleId, organizationId)
|
|
800
|
+
❌ UpdateUserDto (not found - needs creation)
|
|
801
|
+
|
|
802
|
+
🔧 Zod Schemas:
|
|
803
|
+
❌ No validation schemas found
|
|
804
|
+
|
|
805
|
+
🌐 API Services:
|
|
806
|
+
✅ users.service.ts exists
|
|
807
|
+
⚠️ Missing endpoints: PUT /users/{id}, DELETE /users/{id}
|
|
808
|
+
|
|
809
|
+
🪝 Hooks:
|
|
810
|
+
✅ useUsers (correct, type-safe, has query key)
|
|
811
|
+
❌ useUserMutations (not found)
|
|
812
|
+
❌ useCreateUser, useUpdateUser, useDeleteUser (not found)
|
|
813
|
+
|
|
814
|
+
🎨 UI Components (Mobile):
|
|
815
|
+
⚠️ UsersList.tsx (exists but doesn't use FlatList optimally - needs refactor)
|
|
816
|
+
❌ UsersBottomSheet.tsx (not found)
|
|
817
|
+
❌ UserForm.tsx (not found)
|
|
818
|
+
❌ DeleteUserModal.tsx (not found)
|
|
819
|
+
❌ UserFilters.tsx (not found)
|
|
820
|
+
|
|
821
|
+
📱 Screens:
|
|
822
|
+
⚠️ UsersScreen.tsx (exists but incomplete)
|
|
823
|
+
|
|
824
|
+
🧪 Tests:
|
|
825
|
+
❌ No tests found
|
|
826
|
+
|
|
827
|
+
📊 Implementation Score: 35/100
|
|
828
|
+
|
|
829
|
+
💡 Recommendation: REFACTOR + COMPLETE
|
|
830
|
+
- Update 1 existing type (CreateUserDto)
|
|
831
|
+
- Create 1 missing type (UpdateUserDto)
|
|
832
|
+
- Create all validation schemas (3 schemas)
|
|
833
|
+
- Add 2 missing API endpoints
|
|
834
|
+
- Create mutation hooks (3 hooks)
|
|
835
|
+
- Refactor existing list component
|
|
836
|
+
- Create 4 missing components
|
|
837
|
+
- Add comprehensive tests
|
|
838
|
+
|
|
839
|
+
⏱️ Estimated: 13 SP / 8-10 hours
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## Phase 0.5: Project Standards Detection (Mobile-Specific)
|
|
845
|
+
|
|
846
|
+
**CRITICAL: Auto-detect React Native project stack and patterns.**
|
|
847
|
+
|
|
848
|
+
### 1. Read package.json Dependencies
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
const packageJson = await readFile('package.json');
|
|
852
|
+
const { dependencies, devDependencies } = JSON.parse(packageJson);
|
|
853
|
+
|
|
854
|
+
// Detect installed libraries (React Native ecosystem)
|
|
855
|
+
const stack = {
|
|
856
|
+
ui: detectUILibrary(dependencies), // react-native-paper, @rneui, native-base
|
|
857
|
+
list: detectListComponent(dependencies), // FlatList (built-in), SectionList, FlashList
|
|
858
|
+
forms: detectFormsLibrary(dependencies), // react-hook-form
|
|
859
|
+
validation: detectValidation(dependencies), // zod, yup
|
|
860
|
+
query: detectDataFetching(dependencies), // @tanstack/react-query
|
|
861
|
+
state: detectStateManagement(dependencies), // zustand, redux, recoil
|
|
862
|
+
navigation: detectNavigation(dependencies), // @react-navigation/native
|
|
863
|
+
notifications: detectToasts(dependencies), // react-native-toast-message
|
|
864
|
+
};
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### 2. Analyze Existing Components (Reference Pattern)
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
// Search for existing CRUD modules as reference
|
|
871
|
+
const existingModules = await searchFiles('src/features/**/components/**/*.tsx');
|
|
872
|
+
|
|
873
|
+
// Detect patterns from existing code:
|
|
874
|
+
const patterns = {
|
|
875
|
+
list: detectListComponent(existingModules), // FlatList | SectionList | FlashList
|
|
876
|
+
bottomSheet: detectBottomSheetPattern(existingModules), // @gorhom/bottom-sheet | Modal
|
|
877
|
+
filters: detectFiltersPattern(existingModules), // Collapsible | Bottom Sheet | Always Visible
|
|
878
|
+
formLayout: detectFormLayout(existingModules), // Single scroll | Sections
|
|
879
|
+
permissionGuards: detectAuthPattern(existingModules), // useAuth | usePermissions | role checks
|
|
880
|
+
};
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
### 3. Deep UI Pattern Extraction (Mobile-Specific)
|
|
884
|
+
|
|
885
|
+
**Extract EXACT design patterns from existing mobile code:**
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
// Analyze existing list components
|
|
889
|
+
const existingLists = await glob('src/features/**/components/*List.tsx');
|
|
890
|
+
const existingForms = await glob('src/features/**/components/*Form.tsx');
|
|
891
|
+
const existingBottomSheets = await glob('src/features/**/components/*BottomSheet.tsx');
|
|
892
|
+
const existingModals = await glob('src/features/**/components/*Modal.tsx');
|
|
893
|
+
|
|
894
|
+
const extractedDefaults = {
|
|
895
|
+
pagination: {
|
|
896
|
+
pageSize: detectMode(existingLists.map((l) => extractPaginationSize(l))), // Mode = 20 (mobile shows more)
|
|
897
|
+
initialLoad: detectMode(existingLists.map((l) => extractInitialLoad(l))), // 20
|
|
898
|
+
},
|
|
899
|
+
debounce: {
|
|
900
|
+
search: detectMode(existingHooks.map((h) => extractDebounceTime(h))), // 300ms
|
|
901
|
+
},
|
|
902
|
+
list: {
|
|
903
|
+
keyExtractor: detectMode(existingLists.map((l) => extractKeyPattern(l))), // (item) => item.id
|
|
904
|
+
windowSize: detectMode(existingLists.map((l) => extractWindowSize(l))), // 10
|
|
905
|
+
maxToRenderPerBatch: 10,
|
|
906
|
+
updateCellsBatchingPeriod: 50,
|
|
907
|
+
},
|
|
908
|
+
queryOptions: {
|
|
909
|
+
staleTime: detectMode(existingHooks.map((h) => extractStaleTime(h))),
|
|
910
|
+
gcTime: detectMode(existingHooks.map((h) => extractGcTime(h))),
|
|
911
|
+
retry: 1,
|
|
912
|
+
},
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// Deep UI Pattern Analysis (Mobile)
|
|
916
|
+
const uiPatterns = {
|
|
917
|
+
list: {
|
|
918
|
+
contentContainerStyle: extractMode(existingLists, (code) =>
|
|
919
|
+
extractStyleProp(code, 'contentContainerStyle')
|
|
920
|
+
),
|
|
921
|
+
itemSeparator: extractMode(existingLists, (code) =>
|
|
922
|
+
extractComponent(code, 'ItemSeparatorComponent')
|
|
923
|
+
),
|
|
924
|
+
emptyComponent: extractMode(existingLists, (code) =>
|
|
925
|
+
extractComponent(code, 'ListEmptyComponent')
|
|
926
|
+
),
|
|
927
|
+
headerComponent: extractMode(existingLists, (code) =>
|
|
928
|
+
extractComponent(code, 'ListHeaderComponent')
|
|
929
|
+
),
|
|
930
|
+
footerComponent: extractMode(existingLists, (code) =>
|
|
931
|
+
extractComponent(code, 'ListFooterComponent')
|
|
932
|
+
),
|
|
933
|
+
loadingIndicator: extractMode(existingLists, (code) => extractLoadingIndicator(code)),
|
|
934
|
+
},
|
|
935
|
+
form: {
|
|
936
|
+
layout: detectMode(existingForms.map((f) => detectFormLayout(f))), // 'scroll-view' | 'keyboard-avoiding'
|
|
937
|
+
spacing: detectMode(existingForms.map((f) => extractFieldSpacing(f))), // 16, 20, 24
|
|
938
|
+
textInputMode: detectMode(existingForms.map((f) => extractTextInputMode(f))), // 'outlined' | 'flat'
|
|
939
|
+
labelPosition: detectMode(existingForms.map((f) => extractLabelPosition(f))), // 'top' | 'floating'
|
|
940
|
+
buttonAlignment: detectMode(existingForms.map((f) => extractButtonAlignment(f))), // 'bottom' | 'inline'
|
|
941
|
+
validationDisplay: detectMode(existingForms.map((f) => extractValidationStyle(f))), // 'inline' | 'bottom'
|
|
942
|
+
},
|
|
943
|
+
bottomSheet: {
|
|
944
|
+
snapPoints: detectMode(existingBottomSheets.map((bs) => extractSnapPoints(bs))), // ['25%', '50%', '90%']
|
|
945
|
+
index: detectMode(existingBottomSheets.map((bs) => extractInitialIndex(bs))), // 1 (start at 50%)
|
|
946
|
+
enablePanDownToClose: true, // Most common
|
|
947
|
+
backdropComponent: detectMode(existingBottomSheets.map((bs) => extractBackdrop(bs))),
|
|
948
|
+
},
|
|
949
|
+
modal: {
|
|
950
|
+
animationType: detectMode(existingModals.map((m) => extractAnimationType(m))), // 'slide' | 'fade'
|
|
951
|
+
presentationStyle: detectMode(existingModals.map((m) => extractPresentationStyle(m))), // 'pageSheet' | 'formSheet'
|
|
952
|
+
transparent: detectMode(existingModals.map((m) => extractTransparent(m))), // true/false
|
|
953
|
+
},
|
|
954
|
+
reusableComponents: {
|
|
955
|
+
statusBadge: findComponent('StatusBadge'),
|
|
956
|
+
roleChip: findComponent('RoleChip'),
|
|
957
|
+
emptyState: findComponent('EmptyState'),
|
|
958
|
+
loadingSkeleton: findComponent('LoadingSkeleton'),
|
|
959
|
+
},
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// Helper functions (mobile-specific)
|
|
963
|
+
function extractMode<T>(files: string[], extractor: (code: string) => T): T {
|
|
964
|
+
const values = files.map((f) => extractor(readFileSync(f, 'utf-8')));
|
|
965
|
+
return statisticalMode(values); // Most common value
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function extractTextInputMode(code: string): 'outlined' | 'flat' {
|
|
969
|
+
const match = code.match(/<TextInput[^>]+mode="([^"]+)"/);
|
|
970
|
+
return (match?.[1] as any) || 'outlined';
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function extractFormLayout(code: string): 'scroll-view' | 'keyboard-avoiding' | 'section-list' {
|
|
974
|
+
if (code.includes('<KeyboardAvoidingView')) {
|
|
975
|
+
return 'keyboard-avoiding';
|
|
976
|
+
}
|
|
977
|
+
if (code.includes('<SectionList')) {
|
|
978
|
+
return 'section-list';
|
|
979
|
+
}
|
|
980
|
+
return 'scroll-view';
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function extractStyleProp(code: string, prop: string): string {
|
|
984
|
+
const styleMatch = code.match(new RegExp(`${prop}=\\{([^}]+)\\}`));
|
|
985
|
+
return styleMatch?.[1]?.trim() || 'not-found';
|
|
986
|
+
}
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### 4. Reference Components for Consistency
|
|
990
|
+
|
|
991
|
+
Identify existing components to use as templates:
|
|
992
|
+
|
|
993
|
+
```typescript
|
|
994
|
+
const referenceComponents = {
|
|
995
|
+
list: 'src/features/organizations/components/OrganizationsList.tsx',
|
|
996
|
+
screen: 'src/features/organizations/screens/OrganizationsScreen.tsx',
|
|
997
|
+
hooks: 'src/features/users/hooks/useUsers.ts',
|
|
998
|
+
mutations: 'src/features/users/hooks/useUserMutations.ts',
|
|
999
|
+
navigation: 'src/navigation/MainNavigator.tsx',
|
|
1000
|
+
bottomSheet: 'src/components/common/BottomSheet.tsx',
|
|
1001
|
+
};
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
---
|
|
1005
|
+
|
|
1006
|
+
## Phase 1: Analysis Summary (Show Only - Mobile)
|
|
1007
|
+
|
|
1008
|
+
Present detected configuration, API analysis, and implementation audit to user:
|
|
1009
|
+
|
|
1010
|
+
```
|
|
1011
|
+
🔍 Project Stack Detected (Mobile):
|
|
1012
|
+
|
|
1013
|
+
UI: React Native Paper v5.11.0
|
|
1014
|
+
List: FlatList (React Native built-in) + FlashList v1.6.0 ✅
|
|
1015
|
+
Forms: React Hook Form v7.50.0 + Zod v3.22.4 ✅
|
|
1016
|
+
Data: TanStack Query v5.20.0 ✅
|
|
1017
|
+
State: Zustand v4.5.0 ✅
|
|
1018
|
+
Navigation: React Navigation v6.1.0 ✅
|
|
1019
|
+
Toasts: react-native-toast-message v2.1.7 ✅
|
|
1020
|
+
|
|
1021
|
+
📐 UX Standards (from existing code):
|
|
1022
|
+
✅ CRUD Pattern: Bottom Sheet (snap points: 25%, 50%, 90%)
|
|
1023
|
+
✅ List: FlatList with pull-to-refresh, infinite scroll
|
|
1024
|
+
✅ Filters: Bottom Sheet with Apply/Reset buttons
|
|
1025
|
+
✅ Forms: React Hook Form + Zod validation (inline errors)
|
|
1026
|
+
✅ Delete: Modal with confirmation (destructive style)
|
|
1027
|
+
✅ Permissions: Hook-based (useAuthStore + role checks)
|
|
1028
|
+
|
|
1029
|
+
📊 API Module Analysis: Users
|
|
1030
|
+
|
|
1031
|
+
Endpoints Found:
|
|
1032
|
+
✅ GET /api/users (List with pagination)
|
|
1033
|
+
✅ POST /api/users (Create)
|
|
1034
|
+
✅ GET /api/users/{id} (Read)
|
|
1035
|
+
✅ PUT /api/users/{id} (Update)
|
|
1036
|
+
✅ DELETE /api/users/{id} (Delete)
|
|
1037
|
+
|
|
1038
|
+
Entity Schema:
|
|
1039
|
+
Fields (8): id, email, firstName, lastName, status, role, createdAt, updatedAt
|
|
1040
|
+
Required: email, firstName, lastName, roleId
|
|
1041
|
+
Enums: status (active, pending, suspended)
|
|
1042
|
+
Relations (1): role → /api/roles (many-to-one, display: name)
|
|
1043
|
+
|
|
1044
|
+
Features:
|
|
1045
|
+
✅ Server-side pagination (page, limit, max: 100)
|
|
1046
|
+
✅ Search (by name, email)
|
|
1047
|
+
✅ Filters (roleId, status)
|
|
1048
|
+
✅ Authentication (Bearer JWT)
|
|
1049
|
+
✅ Role-based access (ROOT, OWNER, ADMIN)
|
|
1050
|
+
|
|
1051
|
+
✅ All standards locked. Module will match existing patterns.
|
|
1052
|
+
|
|
1053
|
+
Analysis complete. Returning data to flow-work orchestrator...
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
1058
|
+
## 📤 OUTPUT Format (CRITICAL)
|
|
1059
|
+
|
|
1060
|
+
**This sub-prompt MUST return a structured JSON object that flow-work can consume.**
|
|
1061
|
+
|
|
1062
|
+
### OpenAPIAnalysisResult Interface (Mobile)
|
|
1063
|
+
|
|
1064
|
+
```typescript
|
|
1065
|
+
interface OpenAPIAnalysisResult {
|
|
1066
|
+
// Meta
|
|
1067
|
+
success: boolean;
|
|
1068
|
+
module: string;
|
|
1069
|
+
apiUrl: string;
|
|
1070
|
+
timestamp: string; // ISO 8601
|
|
1071
|
+
|
|
1072
|
+
// Implementation Audit
|
|
1073
|
+
implementationAudit: {
|
|
1074
|
+
status: 'NOT_IMPLEMENTED' | 'PARTIAL' | 'COMPLETE';
|
|
1075
|
+
score: number; // 0-100
|
|
1076
|
+
strategy: 'FULL_NEW' | 'REFACTOR_COMPLETE' | 'MINOR_FIXES';
|
|
1077
|
+
types: {
|
|
1078
|
+
matching: string[];
|
|
1079
|
+
missing: Array<{ schema: string; action: string; reason: string }>;
|
|
1080
|
+
incorrect: Array<{
|
|
1081
|
+
schema: string;
|
|
1082
|
+
file: string;
|
|
1083
|
+
issues: string[];
|
|
1084
|
+
action: string;
|
|
1085
|
+
}>;
|
|
1086
|
+
};
|
|
1087
|
+
hooks: {
|
|
1088
|
+
implemented: string[];
|
|
1089
|
+
missing: Array<{
|
|
1090
|
+
endpoint: string;
|
|
1091
|
+
expectedHook: string;
|
|
1092
|
+
action: string;
|
|
1093
|
+
}>;
|
|
1094
|
+
incorrect: Array<{
|
|
1095
|
+
hook: string;
|
|
1096
|
+
file: string;
|
|
1097
|
+
issues: string[];
|
|
1098
|
+
action: string;
|
|
1099
|
+
}>;
|
|
1100
|
+
};
|
|
1101
|
+
components: {
|
|
1102
|
+
complete: string[];
|
|
1103
|
+
missing: Array<{ component: string; action: string }>;
|
|
1104
|
+
nonStandard: Array<{
|
|
1105
|
+
component: string;
|
|
1106
|
+
file: string;
|
|
1107
|
+
issues: string[];
|
|
1108
|
+
action: string;
|
|
1109
|
+
}>;
|
|
1110
|
+
};
|
|
1111
|
+
actionItems: string[]; // Human-readable list
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
// Project Standards (detected - mobile)
|
|
1115
|
+
projectStandards: {
|
|
1116
|
+
stack: {
|
|
1117
|
+
ui: string; // 'react-native-paper v5.11.0'
|
|
1118
|
+
list: string; // 'FlatList (React Native) + FlashList v1.6.0'
|
|
1119
|
+
forms: string; // 'react-hook-form v7.50.0'
|
|
1120
|
+
validation: string; // 'zod v3.22.4'
|
|
1121
|
+
query: string; // '@tanstack/react-query v5.20.0'
|
|
1122
|
+
state: string; // 'zustand v4.5.0'
|
|
1123
|
+
navigation: string; // '@react-navigation/native v6.1.0'
|
|
1124
|
+
notifications: string; // 'react-native-toast-message v2.1.7'
|
|
1125
|
+
};
|
|
1126
|
+
patterns: {
|
|
1127
|
+
crudPattern: string; // 'bottom-sheet' | 'modal' | 'full-screen'
|
|
1128
|
+
bottomSheetSnapPoints: string[]; // ['25%', '50%', '90%']
|
|
1129
|
+
listComponent: string; // 'FlatList' | 'FlashList' | 'SectionList'
|
|
1130
|
+
filterUI: string; // 'bottom-sheet' | 'modal'
|
|
1131
|
+
formLayout: string; // 'scroll-view' | 'keyboard-avoiding'
|
|
1132
|
+
deleteConfirmation: string; // 'modal'
|
|
1133
|
+
};
|
|
1134
|
+
defaults: {
|
|
1135
|
+
pagination: {
|
|
1136
|
+
pageSize: number; // 20 (mobile typically loads more)
|
|
1137
|
+
initialLoad: number; // 20
|
|
1138
|
+
};
|
|
1139
|
+
debounce: {
|
|
1140
|
+
search: number; // 300ms
|
|
1141
|
+
};
|
|
1142
|
+
list: {
|
|
1143
|
+
windowSize: number; // 10
|
|
1144
|
+
maxToRenderPerBatch: number; // 10
|
|
1145
|
+
updateCellsBatchingPeriod: number; // 50
|
|
1146
|
+
};
|
|
1147
|
+
caching: {
|
|
1148
|
+
staleTime: number; // 30000ms (30s)
|
|
1149
|
+
gcTime: number; // 300000ms (5min)
|
|
1150
|
+
retry: number; // 1 for mutations
|
|
1151
|
+
};
|
|
1152
|
+
};
|
|
1153
|
+
referenceComponents: {
|
|
1154
|
+
list: string; // 'src/features/organizations/components/OrganizationsList.tsx'
|
|
1155
|
+
screen: string; // 'src/features/organizations/screens/OrganizationsScreen.tsx'
|
|
1156
|
+
hooks: string; // 'src/features/users/hooks/useUsers.ts'
|
|
1157
|
+
mutations: string; // 'src/features/users/hooks/useUserMutations.ts'
|
|
1158
|
+
};
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
// OpenAPI Analysis
|
|
1162
|
+
openapi: {
|
|
1163
|
+
version: string; // '3.0.3'
|
|
1164
|
+
title: string; // 'CROSS Mobile API'
|
|
1165
|
+
totalPaths: number; // 45
|
|
1166
|
+
totalSchemas: number; // 32
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
// Module Endpoints
|
|
1170
|
+
endpoints: Array<{
|
|
1171
|
+
method: string; // 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'
|
|
1172
|
+
path: string; // '/api/users'
|
|
1173
|
+
operationId: string; // 'getUsers'
|
|
1174
|
+
summary: string; // 'List all users with pagination'
|
|
1175
|
+
tags: string[]; // ['Users']
|
|
1176
|
+
parameters: Array<{
|
|
1177
|
+
name: string; // 'page'
|
|
1178
|
+
in: string; // 'query'
|
|
1179
|
+
required: boolean;
|
|
1180
|
+
schema: {
|
|
1181
|
+
type: string;
|
|
1182
|
+
default?: any;
|
|
1183
|
+
enum?: any[];
|
|
1184
|
+
};
|
|
1185
|
+
}>;
|
|
1186
|
+
requestBody?: {
|
|
1187
|
+
required: boolean;
|
|
1188
|
+
schema: string; // 'CreateUserDto' (schema name)
|
|
1189
|
+
};
|
|
1190
|
+
responses: {
|
|
1191
|
+
[statusCode: string]: {
|
|
1192
|
+
description: string;
|
|
1193
|
+
schema?: string; // 'UserResponseDto' or 'PaginatedResponse<UserResponseDto>'
|
|
1194
|
+
};
|
|
1195
|
+
};
|
|
1196
|
+
}>;
|
|
1197
|
+
|
|
1198
|
+
// Entity Schemas (DTOs)
|
|
1199
|
+
schemas: {
|
|
1200
|
+
response: {
|
|
1201
|
+
name: string; // 'UserResponseDto'
|
|
1202
|
+
fields: FieldSpec[];
|
|
1203
|
+
};
|
|
1204
|
+
create: {
|
|
1205
|
+
name: string; // 'CreateUserDto'
|
|
1206
|
+
fields: FieldSpec[];
|
|
1207
|
+
};
|
|
1208
|
+
update: {
|
|
1209
|
+
name: string; // 'UpdateUserDto'
|
|
1210
|
+
fields: FieldSpec[];
|
|
1211
|
+
};
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
// Field Specifications
|
|
1215
|
+
fields: FieldSpec[]; // All unique fields across DTOs
|
|
1216
|
+
|
|
1217
|
+
// Detected Features
|
|
1218
|
+
features: {
|
|
1219
|
+
pagination: {
|
|
1220
|
+
enabled: boolean;
|
|
1221
|
+
params: string[]; // ['page', 'limit']
|
|
1222
|
+
responseFormat: 'array' | 'object';
|
|
1223
|
+
dataKey?: string; // 'items' | 'data'
|
|
1224
|
+
totalKey?: string; // 'total' | 'meta.total'
|
|
1225
|
+
maxPageSize?: number; // 100
|
|
1226
|
+
};
|
|
1227
|
+
search: {
|
|
1228
|
+
enabled: boolean;
|
|
1229
|
+
params: string[]; // ['search', 'q']
|
|
1230
|
+
};
|
|
1231
|
+
sorting: {
|
|
1232
|
+
enabled: boolean;
|
|
1233
|
+
params: string[]; // ['sortBy', 'order']
|
|
1234
|
+
fields?: string[]; // Sortable fields
|
|
1235
|
+
};
|
|
1236
|
+
filtering: {
|
|
1237
|
+
enabled: boolean;
|
|
1238
|
+
fields: string[]; // ['status', 'roleId']
|
|
1239
|
+
};
|
|
1240
|
+
authentication: {
|
|
1241
|
+
type: 'bearer' | 'apiKey' | 'oauth2';
|
|
1242
|
+
required: boolean;
|
|
1243
|
+
};
|
|
1244
|
+
authorization: {
|
|
1245
|
+
roles: string[]; // ['ROOT', 'OWNER', 'ADMIN']
|
|
1246
|
+
permissions?: string[]; // ['user:read', 'user:write']
|
|
1247
|
+
};
|
|
1248
|
+
softDelete: {
|
|
1249
|
+
enabled: boolean;
|
|
1250
|
+
field?: string; // 'deletedAt' | 'status'
|
|
1251
|
+
};
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
// Relationships
|
|
1255
|
+
relationships: Array<{
|
|
1256
|
+
field: string; // 'roleId' or 'role'
|
|
1257
|
+
relatedEntity: string; // 'Role'
|
|
1258
|
+
type: 'many-to-one' | 'one-to-many' | 'many-to-many' | 'one-to-one';
|
|
1259
|
+
foreignKey: string; // 'roleId'
|
|
1260
|
+
endpoint?: string; // '/api/roles'
|
|
1261
|
+
displayField: string; // 'name' (field to display in UI)
|
|
1262
|
+
populated: boolean; // true if backend returns full object
|
|
1263
|
+
}>;
|
|
1264
|
+
|
|
1265
|
+
// Complexity Analysis
|
|
1266
|
+
complexity: {
|
|
1267
|
+
level: 'SIMPLE' | 'MEDIUM' | 'COMPLEX';
|
|
1268
|
+
estimatedFiles: number; // 18-20
|
|
1269
|
+
estimatedSP: number; // 8
|
|
1270
|
+
estimatedHours: number; // 5-6
|
|
1271
|
+
factors: {
|
|
1272
|
+
endpoints: number; // 5
|
|
1273
|
+
relations: number; // 2
|
|
1274
|
+
customEndpoints: number; // 0
|
|
1275
|
+
validationRules: number; // 15
|
|
1276
|
+
};
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
// Validation & Warnings
|
|
1280
|
+
warnings: string[]; // Issues detected (missing endpoints, inconsistent naming, etc.)
|
|
1281
|
+
suggestions: string[]; // Recommendations
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
interface FieldSpec {
|
|
1285
|
+
name: string; // 'email'
|
|
1286
|
+
type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object' | 'uuid';
|
|
1287
|
+
required: boolean;
|
|
1288
|
+
nullable: boolean;
|
|
1289
|
+
validation?: {
|
|
1290
|
+
min?: number;
|
|
1291
|
+
max?: number;
|
|
1292
|
+
pattern?: string;
|
|
1293
|
+
format?: 'email' | 'url' | 'uuid' | 'date-time' | 'password';
|
|
1294
|
+
enum?: string[];
|
|
1295
|
+
};
|
|
1296
|
+
relation?: {
|
|
1297
|
+
entity: string; // 'Role'
|
|
1298
|
+
type: 'many-to-one' | 'one-to-many' | 'many-to-many';
|
|
1299
|
+
foreignKey: string; // 'roleId'
|
|
1300
|
+
populated: boolean;
|
|
1301
|
+
displayField: string; // 'name'
|
|
1302
|
+
};
|
|
1303
|
+
category: 'auto-generated' | 'read-only' | 'editable' | 'metadata';
|
|
1304
|
+
usage: {
|
|
1305
|
+
showInList: boolean; // Mobile uses "list" not "table"
|
|
1306
|
+
showInForm: boolean; // In create/edit forms
|
|
1307
|
+
showInDetails: boolean;
|
|
1308
|
+
editable: boolean; // Can be edited after creation
|
|
1309
|
+
};
|
|
1310
|
+
default?: any;
|
|
1311
|
+
description?: string;
|
|
1312
|
+
|
|
1313
|
+
// DTO specific
|
|
1314
|
+
inResponseDto: boolean;
|
|
1315
|
+
inCreateDto: boolean;
|
|
1316
|
+
inUpdateDto: boolean;
|
|
1317
|
+
}
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
### Return Format
|
|
1321
|
+
|
|
1322
|
+
```json
|
|
1323
|
+
{
|
|
1324
|
+
"success": true,
|
|
1325
|
+
"module": "users",
|
|
1326
|
+
"apiUrl": "http://localhost:3001/api/docs-json",
|
|
1327
|
+
"timestamp": "2026-03-04T10:30:00-03:00",
|
|
1328
|
+
"projectStandards": {
|
|
1329
|
+
/* ... mobile-specific */
|
|
1330
|
+
},
|
|
1331
|
+
"openapi": {
|
|
1332
|
+
/* ... */
|
|
1333
|
+
},
|
|
1334
|
+
"endpoints": [
|
|
1335
|
+
/* ... */
|
|
1336
|
+
],
|
|
1337
|
+
"schemas": {
|
|
1338
|
+
/* ... */
|
|
1339
|
+
},
|
|
1340
|
+
"fields": [
|
|
1341
|
+
/* ... */
|
|
1342
|
+
],
|
|
1343
|
+
"features": {
|
|
1344
|
+
/* ... */
|
|
1345
|
+
},
|
|
1346
|
+
"relationships": [
|
|
1347
|
+
/* ... */
|
|
1348
|
+
],
|
|
1349
|
+
"complexity": {
|
|
1350
|
+
/* ... */
|
|
1351
|
+
},
|
|
1352
|
+
"warnings": [],
|
|
1353
|
+
"suggestions": []
|
|
1354
|
+
}
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
---
|
|
1358
|
+
|
|
1359
|
+
## Best Practices Reference (For flow-work - Mobile)
|
|
1360
|
+
|
|
1361
|
+
**These patterns should be enforced by flow-work during mobile code generation:**
|
|
1362
|
+
|
|
1363
|
+
### 1. Use FlatList or FlashList (React Native Standard)
|
|
1364
|
+
|
|
1365
|
+
```typescript
|
|
1366
|
+
// ✅ CORRECT: Use FlatList with optimization
|
|
1367
|
+
import { FlatList, ActivityIndicator } from 'react-native';
|
|
1368
|
+
|
|
1369
|
+
<FlatList
|
|
1370
|
+
data={users}
|
|
1371
|
+
keyExtractor={(item) => item.id}
|
|
1372
|
+
renderItem={({ item }) => <UserListItem user={item} />}
|
|
1373
|
+
onEndReached={loadMore}
|
|
1374
|
+
onEndReachedThreshold={0.3}
|
|
1375
|
+
refreshing={isRefreshing}
|
|
1376
|
+
onRefresh={refetch}
|
|
1377
|
+
ListEmptyComponent={<EmptyState />}
|
|
1378
|
+
ListFooterComponent={isLoading ? <ActivityIndicator /> : null}
|
|
1379
|
+
windowSize={10}
|
|
1380
|
+
maxToRenderPerBatch={10}
|
|
1381
|
+
updateCellsBatchingPeriod={50}
|
|
1382
|
+
/>
|
|
1383
|
+
|
|
1384
|
+
// ❌ WRONG: Don't use ScrollView with map for long lists
|
|
1385
|
+
// <ScrollView>{users.map(user => <UserItem user={user} />)}</ScrollView>
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
### 2. Query Key Management
|
|
1389
|
+
|
|
1390
|
+
```typescript
|
|
1391
|
+
// ✅ CORRECT: Export query key constant
|
|
1392
|
+
export const USERS_QUERY_KEY = 'users';
|
|
1393
|
+
|
|
1394
|
+
export const useUsers = (params: GetUsersParams) => {
|
|
1395
|
+
return useQuery({
|
|
1396
|
+
queryKey: [USERS_QUERY_KEY, params],
|
|
1397
|
+
queryFn: () => usersService.getUsers(params),
|
|
1398
|
+
});
|
|
1399
|
+
};
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
### 3. Cache Invalidation Strategy (Broad)
|
|
1403
|
+
|
|
1404
|
+
```typescript
|
|
1405
|
+
// ✅ CORRECT: Invalidate ALL related queries
|
|
1406
|
+
void queryClient.invalidateQueries({ queryKey: [USERS_QUERY_KEY] });
|
|
1407
|
+
|
|
1408
|
+
// ❌ WRONG: Too specific (misses cached list queries)
|
|
1409
|
+
// void queryClient.invalidateQueries({ queryKey: [USERS_QUERY_KEY, id] });
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
### 4. Toast Message Standards (Mobile)
|
|
1413
|
+
|
|
1414
|
+
```typescript
|
|
1415
|
+
// ✅ CREATE success
|
|
1416
|
+
Toast.show({
|
|
1417
|
+
type: 'success',
|
|
1418
|
+
text1: 'Usuario creado',
|
|
1419
|
+
text2: 'Email de bienvenida enviado',
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
// ✅ UPDATE success
|
|
1423
|
+
Toast.show({
|
|
1424
|
+
type: 'success',
|
|
1425
|
+
text1: 'Usuario actualizado',
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
// ✅ ERROR with status
|
|
1429
|
+
if (error.response?.status === 409) {
|
|
1430
|
+
Toast.show({
|
|
1431
|
+
type: 'error',
|
|
1432
|
+
text1: 'Error',
|
|
1433
|
+
text2: 'No puedes suspender al último OWNER activo',
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
```
|
|
1437
|
+
|
|
1438
|
+
### 5. Loading States (Mobile)
|
|
1439
|
+
|
|
1440
|
+
```typescript
|
|
1441
|
+
// ✅ Proper loading states for FlatList
|
|
1442
|
+
<FlatList
|
|
1443
|
+
data={data?.items ?? []}
|
|
1444
|
+
refreshing={isRefreshing} // Pull-to-refresh
|
|
1445
|
+
onRefresh={refetch}
|
|
1446
|
+
ListFooterComponent={
|
|
1447
|
+
isFetchingNextPage ? <ActivityIndicator /> : null
|
|
1448
|
+
}
|
|
1449
|
+
ListEmptyComponent={
|
|
1450
|
+
isLoading ? <LoadingSkeleton /> : <EmptyState />
|
|
1451
|
+
}
|
|
1452
|
+
/>
|
|
1453
|
+
```
|
|
1454
|
+
|
|
1455
|
+
---
|
|
1456
|
+
|
|
1457
|
+
## Error Handling
|
|
1458
|
+
|
|
1459
|
+
**If analysis fails at any step:**
|
|
1460
|
+
|
|
1461
|
+
```json
|
|
1462
|
+
{
|
|
1463
|
+
"success": false,
|
|
1464
|
+
"module": "users",
|
|
1465
|
+
"error": "Failed to fetch OpenAPI spec",
|
|
1466
|
+
"details": "Connection timeout after 10 seconds",
|
|
1467
|
+
"suggestions": [
|
|
1468
|
+
"1. Ensure backend server is running and accessible from device",
|
|
1469
|
+
"2. Check network configuration (WiFi, mobile data)",
|
|
1470
|
+
"3. Verify API URL is correct (not localhost if testing on real device)",
|
|
1471
|
+
"4. Try --api-url=http://your-computer-ip:3001/api/docs-json"
|
|
1472
|
+
]
|
|
1473
|
+
}
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
**flow-work should handle this by:**
|
|
1477
|
+
|
|
1478
|
+
1. Showing error to user
|
|
1479
|
+
2. Offering retry or manual mode
|
|
1480
|
+
3. Logging error for debugging
|
|
1481
|
+
|
|
1482
|
+
---
|
|
1483
|
+
|
|
1484
|
+
## End of Sub-Prompt
|
|
1485
|
+
|
|
1486
|
+
**This prompt returns control to `flow-work` with the `OpenAPIAnalysisResult` data structure.**
|
|
1487
|
+
|
|
1488
|
+
Flow-work will use this data to:
|
|
1489
|
+
|
|
1490
|
+
- Generate detailed `work.md` (Phase 2)
|
|
1491
|
+
- Create branch with naming convention (Phase 3)
|
|
1492
|
+
- Execute implementation (Phase 3)
|
|
1493
|
+
- Validate and finalize (Phase 4)
|