@atlashub/smartstack-cli 4.30.0 → 4.31.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 (40) hide show
  1. package/dist/index.js +17 -4
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/skills/apex/references/code-generation.md +1 -1
  5. package/templates/skills/apex/references/person-extension-pattern.md +23 -2
  6. package/templates/skills/apex/references/post-checks.md +52 -0
  7. package/templates/skills/apex/references/smartstack-api.md +111 -0
  8. package/templates/skills/apex/references/smartstack-frontend.md +25 -2
  9. package/templates/skills/apex/references/smartstack-layers.md +1 -0
  10. package/templates/skills/apex/steps/step-03-execute.md +110 -7
  11. package/templates/skills/application/templates-frontend.md +1 -1
  12. package/templates/skills/ba-generate-html/SKILL.md +1 -1
  13. package/templates/skills/ba-generate-html/html/ba-interactive.html +42 -78
  14. package/templates/skills/ba-generate-html/html/src/partials/cadrage-scope.html +14 -36
  15. package/templates/skills/ba-generate-html/html/src/partials/decomp-modules.html +0 -8
  16. package/templates/skills/ba-generate-html/html/src/scripts/01-data-init.js +20 -20
  17. package/templates/skills/ba-generate-html/html/src/scripts/03-render-cadrage.js +4 -3
  18. package/templates/skills/ba-generate-html/html/src/scripts/04-render-modules.js +0 -2
  19. package/templates/skills/ba-generate-html/html/src/scripts/07-render-handoff.js +2 -5
  20. package/templates/skills/ba-generate-html/html/src/scripts/10-comments.js +1 -1
  21. package/templates/skills/ba-generate-html/html/src/styles/04-cards.css +2 -4
  22. package/templates/skills/ba-generate-html/html/src/template.html +14 -44
  23. package/templates/skills/ba-generate-html/references/data-build.md +4 -9
  24. package/templates/skills/ba-generate-html/references/data-mapping.md +2 -7
  25. package/templates/skills/ba-generate-html/references/output-modes.md +1 -1
  26. package/templates/skills/ba-generate-html/steps/step-02-build-data.md +3 -6
  27. package/templates/skills/ba-generate-html/steps/step-04-verify.md +2 -2
  28. package/templates/skills/ba-review/references/review-data-mapping.md +4 -6
  29. package/templates/skills/ba-review/steps/step-01-apply.md +2 -4
  30. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +4 -4
  31. package/templates/skills/business-analyse/questionnaire.md +1 -1
  32. package/templates/skills/business-analyse/react/schema.md +2 -7
  33. package/templates/skills/business-analyse/schemas/application-schema.json +2 -9
  34. package/templates/skills/business-analyse/schemas/project-schema.json +4 -8
  35. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +1 -3
  36. package/templates/skills/business-analyse/steps/step-01-cadrage.md +5 -12
  37. package/templates/skills/business-analyse/steps/step-02-structure.md +3 -5
  38. package/templates/skills/dev-start/SKILL.md +242 -0
  39. package/templates/skills/ui-components/SKILL.md +1 -1
  40. package/templates/skills/ui-components/patterns/data-table.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.30.0",
3
+ "version": "4.31.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -335,7 +335,7 @@ public class EmployeeService : IEmployeeService
335
335
  _db.Employees.Add(entity);
336
336
  await _db.SaveChangesAsync(ct);
337
337
 
338
- return MapToDto(entity);
338
+ return new EmployeeResponseDto(entity.Id, entity.Code, entity.Name, entity.CreatedAt);
339
339
  }
340
340
  }
341
341
  ```
@@ -401,7 +401,17 @@ public class CustomerService : ICustomerService
401
401
  _db.Customers.Add(entity);
402
402
  await _db.SaveChangesAsync(ct);
403
403
 
404
- return MapToDto(entity);
404
+ // Reload with User for response
405
+ var created = await _db.Customers
406
+ .Include(x => x.User)
407
+ .FirstAsync(x => x.Id == entity.Id, ct);
408
+
409
+ return new CustomerResponseDto(
410
+ created.Id, created.Code, created.Status, created.CompanyName,
411
+ created.User != null ? created.User.FirstName : created.FirstName,
412
+ created.User != null ? created.User.LastName : created.LastName,
413
+ created.User != null ? created.User.Email : created.Email,
414
+ created.UserId);
405
415
  }
406
416
  }
407
417
  ```
@@ -508,7 +518,18 @@ public record CustomerResponseDto(
508
518
  />
509
519
 
510
520
  // Business fields only — no person fields
511
- <input type="date" value={formData.hireDate} ... />
521
+ <div>
522
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
523
+ {t('module:form.hireDate', 'Hire Date')}
524
+ </label>
525
+ <input
526
+ type="date"
527
+ value={formData.hireDate}
528
+ onChange={(e) => handleChange('hireDate', e.target.value)}
529
+ required
530
+ className="w-full px-3 py-2 rounded-[var(--radius-input)] border border-[var(--border-color)] bg-[var(--bg-tertiary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)]"
531
+ />
532
+ </div>
512
533
  ```
513
534
 
514
535
  ### Optional — CreatePage
@@ -1926,6 +1926,58 @@ if [ -n "$APP_TSX" ]; then
1926
1926
  fi
1927
1927
  ```
1928
1928
 
1929
+ ### POST-CHECK C53: Enum serialization — JsonStringEnumConverter required (BLOCKING)
1930
+
1931
+ > **Source:** Frontend sends enum values as strings ("PaidLeave", "Morning") but C# enums serialize
1932
+ > as integers by default. Without `JsonStringEnumConverter`, the JSON binding fails → 400 Bad Request
1933
+ > on create/update endpoints. The enum check in step-03 Layer 0 can be skipped by the LLM — this
1934
+ > POST-CHECK catches it as defense-in-depth.
1935
+
1936
+ ```bash
1937
+ # Check if global JsonStringEnumConverter exists in Program.cs
1938
+ PROGRAM_CS=$(find src/ -name "Program.cs" -path "*/Api/*" 2>/dev/null | head -1)
1939
+ HAS_GLOBAL_CONVERTER=false
1940
+ if [ -n "$PROGRAM_CS" ] && grep -q "JsonStringEnumConverter" "$PROGRAM_CS" 2>/dev/null; then
1941
+ HAS_GLOBAL_CONVERTER=true
1942
+ fi
1943
+
1944
+ if [ "$HAS_GLOBAL_CONVERTER" = false ]; then
1945
+ # No global converter — every enum in Domain/Enums must have the attribute
1946
+ ENUM_FILES=$(find src/ -path "*/Domain/Enums/*" -name "*.cs" 2>/dev/null)
1947
+ if [ -n "$ENUM_FILES" ]; then
1948
+ for f in $ENUM_FILES; do
1949
+ if grep -q "public enum" "$f" && ! grep -q "JsonStringEnumConverter" "$f"; then
1950
+ echo "BLOCKING: Enum missing [JsonConverter(typeof(JsonStringEnumConverter))]: $f"
1951
+ echo " Frontend sends enum values as strings but C# deserializes as int by default"
1952
+ echo " Fix: Add [JsonConverter(typeof(JsonStringEnumConverter))] on the enum"
1953
+ echo " Or: Add JsonStringEnumConverter globally in Program.cs"
1954
+ fi
1955
+ done
1956
+ fi
1957
+ fi
1958
+ ```
1959
+
1960
+ ### POST-CHECK C54: No helper method calls inside .Select() on IQueryable (BLOCKING)
1961
+
1962
+ > **Source:** Service `GetAllAsync()` used `.Select(a => MapToDto(a))` — EF Core cannot translate
1963
+ > helper method calls to SQL → runtime `InvalidOperationException` or `NullReferenceException`
1964
+ > (because `.Include()` is ignored when `.Select()` is present, and the helper expects loaded navigations).
1965
+
1966
+ ```bash
1967
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
1968
+ if [ -n "$SERVICE_FILES" ]; then
1969
+ # Look for .Select(x => MethodName(x)) or .Select(MethodName) patterns on IQueryable
1970
+ BAD_SELECT=$(grep -Pn '\.Select\(\s*\w+\s*=>\s*(?!new\s)[A-Z]\w+\(|\.Select\(\s*(?!x\s*=>)[A-Z]\w+\s*\)' $SERVICE_FILES 2>/dev/null)
1971
+ if [ -n "$BAD_SELECT" ]; then
1972
+ echo "BLOCKING: Helper method call inside .Select() on IQueryable — EF Core cannot translate to SQL"
1973
+ echo "$BAD_SELECT"
1974
+ echo "Fix: Use inline DTO construction: .Select(x => new ResponseDto(x.Id, x.Name, x.FK.Code))"
1975
+ echo " Helper methods (MapToDto, ToDto) are only safe AFTER materialization (ToListAsync, FirstAsync)"
1976
+ exit 1
1977
+ fi
1978
+ fi
1979
+ ```
1980
+
1929
1981
  ---
1930
1982
 
1931
1983
  ## Architecture — Clean Architecture Layer Isolation
@@ -72,6 +72,21 @@ public enum EntityScope
72
72
  }
73
73
  ```
74
74
 
75
+ ### Enum Serialization (MANDATORY)
76
+
77
+ All enum types used in DTOs MUST serialize as strings for frontend compatibility.
78
+
79
+ **Option A — Per-enum attribute (preferred when few enums):**
80
+ ```csharp
81
+ [JsonConverter(typeof(JsonStringEnumConverter))]
82
+ public enum EmployeeStatus { Active, OnLeave, Terminated }
83
+ ```
84
+
85
+ **Option B — Global configuration (if project has many enums):**
86
+ Verify in `Program.cs`: `JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())`
87
+
88
+ > **Rule:** Before creating enums, check if global `JsonStringEnumConverter` exists. If yes, no per-enum attribute needed. If no, add `[JsonConverter(typeof(JsonStringEnumConverter))]` on every enum used in API responses.
89
+
75
90
  ---
76
91
 
77
92
  ## Entity Pattern (tenant-scoped, most common)
@@ -473,6 +488,93 @@ public class {Name}Service : I{Name}Service
473
488
  }
474
489
  ```
475
490
 
491
+ ### Service Pattern — Entity with FK to another business entity
492
+
493
+ When an entity has a FK to another business entity (e.g., `Absence.EmployeeId → Employee`), use navigation properties **directly inside `.Select()`**. EF Core translates navigation access to SQL JOINs automatically — `.Include()` is NOT needed when using `.Select()`.
494
+
495
+ ```csharp
496
+ // Example: Absence has FK EmployeeId → Employee
497
+ public async Task<PaginatedResult<AbsenceResponseDto>> GetAllAsync(
498
+ string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
499
+ {
500
+ var tenantId = _currentTenant.TenantId
501
+ ?? throw new TenantContextRequiredException();
502
+
503
+ var query = _db.Absences
504
+ .Where(x => x.TenantId == tenantId)
505
+ .AsNoTracking();
506
+
507
+ if (!string.IsNullOrWhiteSpace(search))
508
+ {
509
+ query = query.Where(x =>
510
+ x.Employee.Code.Contains(search) || // Navigate FK in Where — EF translates to JOIN
511
+ x.Employee.User!.LastName.Contains(search));
512
+ }
513
+
514
+ // CORRECT: Inline construction — access FK navigation directly in Select()
515
+ // EF Core generates the JOIN automatically. Do NOT use .Include() here.
516
+ return await query
517
+ .OrderByDescending(x => x.StartDate)
518
+ .Select(x => new AbsenceResponseDto(
519
+ x.Id, x.StartDate, x.EndDate, x.Type,
520
+ x.EmployeeId, x.Employee.Code,
521
+ x.Employee.User!.FirstName, x.Employee.User!.LastName))
522
+ .ToPaginatedResultAsync(page, pageSize, ct);
523
+ }
524
+
525
+ // GetByIdAsync — same inline Select pattern
526
+ public async Task<AbsenceResponseDto?> GetByIdAsync(Guid id, CancellationToken ct)
527
+ {
528
+ var tenantId = _currentTenant.TenantId
529
+ ?? throw new TenantContextRequiredException();
530
+
531
+ return await _db.Absences
532
+ .Where(x => x.Id == id && x.TenantId == tenantId)
533
+ .AsNoTracking()
534
+ .Select(x => new AbsenceResponseDto(
535
+ x.Id, x.StartDate, x.EndDate, x.Type,
536
+ x.EmployeeId, x.Employee.Code,
537
+ x.Employee.User!.FirstName, x.Employee.User!.LastName))
538
+ .FirstOrDefaultAsync(ct);
539
+ }
540
+
541
+ // CreateAsync — use .Include() AFTER materialization (need full entity for response)
542
+ public async Task<AbsenceResponseDto> CreateAsync(CreateAbsenceDto dto, CancellationToken ct)
543
+ {
544
+ var tenantId = _currentTenant.TenantId
545
+ ?? throw new TenantContextRequiredException();
546
+
547
+ // Verify FK target exists in same tenant
548
+ var employeeExists = await _db.Employees
549
+ .AnyAsync(e => e.Id == dto.EmployeeId && e.TenantId == tenantId, ct);
550
+ if (!employeeExists)
551
+ throw new KeyNotFoundException($"Employee {dto.EmployeeId} not found");
552
+
553
+ var entity = Absence.Create(tenantId, dto.EmployeeId, dto.StartDate, dto.EndDate, dto.Type);
554
+ entity.CreatedBy = _currentUser.UserId?.ToString();
555
+
556
+ _db.Absences.Add(entity);
557
+ await _db.SaveChangesAsync(ct);
558
+
559
+ // Reload with FK navigation for response
560
+ var created = await _db.Absences
561
+ .Include(x => x.Employee).ThenInclude(e => e.User)
562
+ .FirstAsync(x => x.Id == entity.Id, ct);
563
+
564
+ return new AbsenceResponseDto(
565
+ created.Id, created.StartDate, created.EndDate, created.Type,
566
+ created.EmployeeId, created.Employee.Code,
567
+ created.Employee.User!.FirstName, created.Employee.User!.LastName);
568
+ }
569
+ ```
570
+
571
+ > **Rules for FK inter-entity services:**
572
+ > - **GetAllAsync / GetByIdAsync:** Use `.Select()` with inline navigation access (e.g., `x.Employee.Code`). Do NOT use `.Include()` — it is ignored when `.Select()` is present. EF Core generates the JOIN from the navigation path.
573
+ > - **CreateAsync / UpdateAsync:** Use `.Include()` AFTER `SaveChangesAsync()` to reload the entity with its FK navigation for the response DTO.
574
+ > - **FK validation:** Always verify the FK target exists in the same tenant before creating/updating.
575
+ > - **Navigation depth:** Limit to 1-2 levels (e.g., `x.Employee.User.LastName`). Deeper navigation generates complex JOINs — consider denormalization if needed.
576
+ > - **Search:** Include FK display fields in the search filter (e.g., `x.Employee.Code.Contains(search)`).
577
+
476
578
  **Key interfaces (from SmartStack NuGet package):**
477
579
  - `ICurrentUserService` (from `SmartStack.Application.Common.Interfaces.Identity`): provides `UserId` (Guid?), `Email` (string?), `IsAuthenticated` (bool)
478
580
  - `ICurrentTenantService` (from `SmartStack.Application.Common.Interfaces.Tenants`): provides `TenantId` (Guid?), `HasTenant` (bool), `TenantSlug` (string?)
@@ -933,6 +1035,15 @@ public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
933
1035
  }
934
1036
  ```
935
1037
 
1038
+ > **WARNING — IQueryable .Select() constraints:**
1039
+ > Inside `.Select()` on an `IQueryable` (before `ToListAsync`/`FirstAsync`), use ONLY inline DTO construction:
1040
+ > `.Select(x => new ResponseDto(x.Id, x.Code, x.Name))`
1041
+ >
1042
+ > NEVER call a helper method (e.g., `MapToDto(x)`) inside `.Select()` — EF Core cannot translate method calls to SQL → runtime `InvalidOperationException`.
1043
+ > Helper methods are fine AFTER materialization (`ToListAsync`, `FirstAsync`, `SaveChangesAsync`).
1044
+ >
1045
+ > **FK navigation in `.Select()` is safe:** `.Select(x => new Dto(x.Id, x.Employee.Code, x.Employee.User!.LastName))` — EF Core translates navigation property access to SQL JOINs. `.Include()` is NOT needed and is ignored when `.Select()` is used.
1046
+
936
1047
  ### Frontend Types (`@/types/pagination.ts`)
937
1048
 
938
1049
  ```typescript
@@ -274,7 +274,7 @@ export function EntityListPage() {
274
274
  setLoading(true);
275
275
  setError(null);
276
276
  const result = await entityApi.getAll();
277
- setData(result.items);
277
+ setData(result?.items ?? []);
278
278
  } catch (err: any) {
279
279
  setError(err.message || t('{module}:errors.loadFailed', 'Failed to load data'));
280
280
  } finally {
@@ -364,6 +364,11 @@ export function EntityListPage() {
364
364
  }
365
365
  ```
366
366
 
367
+ > **DEFENSIVE RULE — API Response Guards:**
368
+ > Always use `result?.items ?? []` when extracting arrays from API responses.
369
+ > SmartStack's axios interceptor may resolve with `undefined` on auth failures (401/403)
370
+ > instead of rejecting the promise. Without the guard, `data.length` will crash.
371
+
367
372
  ### Detail Page Pattern
368
373
 
369
374
  ```tsx
@@ -875,9 +880,27 @@ When generating form fields, determine the field type from the entity property:
875
880
  | `Guid` (FK — e.g., `EmployeeId`, `DepartmentId`) | **Entity Lookup** | `<EntityLookup />` |
876
881
  | `bool` | Toggle/Checkbox | `<input type="checkbox" />` |
877
882
  | `int` / `decimal` | Number input | `<input type="number" />` |
878
- | `DateTime` | Date picker | `<input type="date" />` |
883
+ | `DateTime` / `DateOnly` | Date input | `<input type="date" className="..." />` — wrap with label + error state using CSS variables |
879
884
  | `enum` | Select dropdown | `<select>` |
880
885
 
886
+ #### Date Field Pattern (styled with CSS variables)
887
+
888
+ ```tsx
889
+ {/* Date field — styled with CSS variables */}
890
+ <div>
891
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
892
+ {t('{module}:form.startDate', 'Start Date')}
893
+ </label>
894
+ <input
895
+ type="date"
896
+ value={formData.startDate}
897
+ onChange={(e) => handleChange('startDate', e.target.value)}
898
+ required
899
+ className="w-full px-3 py-2 rounded-[var(--radius-input)] border border-[var(--border-color)] bg-[var(--bg-tertiary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)]"
900
+ />
901
+ </div>
902
+ ```
903
+
881
904
  **How to detect FK fields:** Any property named `{Entity}Id` of type `Guid` that has a corresponding navigation property is a foreign key. Examples: `EmployeeId`, `DepartmentId`, `CategoryId`, `ParentId`.
882
905
 
883
906
  ### EntityLookup Component Pattern
@@ -59,6 +59,7 @@ Web → Application (via API clients)
59
59
  - No Code/IsDeleted/RowVersion in BaseEntity — add business properties yourself
60
60
  - Domain events for state changes
61
61
  - Value objects for composite values
62
+ - **Enum serialization:** All enums in DTOs must serialize as strings. Check if `JsonStringEnumConverter` is configured globally. If not, add `[JsonConverter(typeof(JsonStringEnumConverter))]` on each enum. Frontend sends/receives strings (e.g., `"Active"`, not `0`).
62
63
  - **Person Extension Pattern:** If entity has `personRoleConfig` (from PRD or feature.json):
63
64
  - Load `references/person-extension-pattern.md` for full patterns
64
65
  - Mandatory variant (`userLinkMode: 'mandatory'`): `Guid UserId` (non-nullable), ZERO person fields (`FirstName`, `LastName`, `Email`), unique index `(TenantId, UserId)`
@@ -64,6 +64,7 @@ BEFORE starting Layer N:
64
64
  - {module_code}: Glob("src/**/Domain/Entities/*/") → target module directory
65
65
  - {entities}: Glob("src/**/Domain/Entities/{module_code}/*.cs") → entity names
66
66
  - {sections}: Glob("src/**/Seeding/Data/{module_code}/*NavigationSeedData.cs") → parse GetSectionEntries
67
+ - {code_patterns}: Read state.json code_patterns field, OR re-derive from existing ICodeGenerator registrations in DependencyInjection.cs, OR default to "manual" for all entities
67
68
  3. IF Layer N-1 was already completed (check state.json or git log):
68
69
  → Skip to Layer N directly
69
70
 
@@ -89,6 +90,36 @@ For each entity to create/modify:
89
90
  → Verify entity matches patterns in references/smartstack-api.md
90
91
  ```
91
92
 
93
+ ### Code Generation Integration
94
+
95
+ ```
96
+ For each entity with auto-generated code ({code_patterns} from step-00):
97
+ IF {code_patterns} has entry for this entity AND strategy != "manual":
98
+ → Pass codePattern in scaffold_extension options:
99
+ MCP scaffold_extension (type: "entity", target: entity_name, options: {
100
+ codePattern: {
101
+ strategy: {code_patterns[entity].strategy},
102
+ prefix: {code_patterns[entity].prefix},
103
+ digits: {code_patterns[entity].digits},
104
+ includeTenantSlug: {code_patterns[entity].includeTenantSlug},
105
+ separator: {code_patterns[entity].separator}
106
+ }
107
+ })
108
+ → Verify: Code property exists on entity but is NOT in CreateDto
109
+ → Verify: ICodeGenerator<{Entity}> is ready for DI registration (Layer 2)
110
+ ELSE:
111
+ → Default behavior (strategy: "manual", Code in CreateDto)
112
+ ```
113
+
114
+ ### Enum Serialization Check
115
+
116
+ ```
117
+ For each enum type created in Domain/Enums/:
118
+ 1. Grep("JsonStringEnumConverter", "src/**/Program.cs")
119
+ 2. IF global config exists → no action
120
+ 3. IF no global config → add [JsonConverter(typeof(JsonStringEnumConverter))] on each enum
121
+ ```
122
+
92
123
  ### Person Extension Detection
93
124
 
94
125
  **If entity has personRoleConfig (mandatory or optional UserId link):**
@@ -247,6 +278,30 @@ TaskUpdate(taskId: progress_tracker_id,
247
278
  - Services/DTOs: MCP scaffold_extension
248
279
  - Controllers: use /controller skill for complex, MCP scaffold_extension for simple
249
280
  - Important: All GetAll endpoints must support `?search=` query parameter (enables EntityLookup on frontend)
281
+ - Guard: DTO mapping in services MUST use inline construction (`new ResponseDto(...)`) inside IQueryable `.Select()`. Never use helper methods (MapToDto, ToDto) inside `.Select()` — EF Core cannot translate them. Helper methods are allowed only after materialization (ToListAsync, FirstAsync).
282
+
283
+ ### Code Generation Service Registration (per entity)
284
+
285
+ ```
286
+ For each entity where {code_patterns} defines strategy != "manual":
287
+ 1. Verify DI registration in DependencyInjection.cs:
288
+ → services.AddScoped<ICodeGenerator<{Entity}>>(sp => new CodeGenerator<{Entity}>(...))
289
+ → Use CodePatternConfig matching {code_patterns[entity]} values
290
+ → See references/code-generation.md "DI Registration Pattern"
291
+ → Guard: Do NOT duplicate if ICodeGenerator<{Entity}> is already registered
292
+ 2. Verify service injection:
293
+ → {Entity}Service constructor receives ICodeGenerator<{Entity}>
294
+ → CreateAsync uses _codeGenerator.NextCodeAsync(ct) instead of dto.Code
295
+ → See references/code-generation.md "Service Integration Pattern"
296
+ 3. Verify CreateDto:
297
+ → Code property MUST NOT be in Create{Entity}Dto
298
+ → Code property MUST be in {Entity}ResponseDto
299
+ → See references/code-generation.md "CreateDto Changes"
300
+ 4. Verify Validator:
301
+ → Create{Entity}Validator has NO Code rule (auto-generated)
302
+ → Update{Entity}Validator has Code rule with regex ^[a-z0-9_-]+$ (if Code is mutable)
303
+ ```
304
+ - FK inter-entity pattern: When an entity has FK to another business entity (e.g., Absence → Employee), use navigation properties directly inside `.Select()` (e.g., `x.Employee.Code`, `x.Employee.User!.LastName`). EF Core translates navigation access to SQL JOINs automatically. Do NOT use `.Include()` with `.Select()` — it is ignored. See `references/smartstack-api.md` "Service Pattern — Entity with FK to another business entity" for the full pattern.
250
305
 
251
306
  ### Skill Delegation
252
307
 
@@ -272,6 +327,13 @@ IF NOT economy_mode AND entities.length > 1:
272
327
  - Controller: /controller skill or MCP scaffold_extension
273
328
  - IMPORTANT: GetAll endpoint MUST support ?search= parameter
274
329
  - Validators: FluentValidation + DI registration
330
+ - CODE GENERATION: {code_patterns[EntityName] summary — e.g., "strategy: sequential, prefix: emp, digits: 5" or "manual"}
331
+ If strategy != "manual": read references/code-generation.md, then:
332
+ → Register ICodeGenerator<{EntityName}> in DI (DependencyInjection.cs) with CodePatternConfig matching {code_patterns}
333
+ → Inject ICodeGenerator<{EntityName}> in {EntityName}Service, use _codeGenerator.NextCodeAsync(ct) in CreateAsync
334
+ → Remove Code from Create{EntityName}Dto (auto-generated, not user-provided)
335
+ → Keep Code in {EntityName}ResponseDto
336
+ → Create{EntityName}Validator: NO Code rule. Update{EntityName}Validator: Code rule with regex ^[a-z0-9_-]+$
275
337
  - Your task ID is {task_id}. Call TaskUpdate(status: "in_progress") before starting.
276
338
  - Call TaskUpdate(status: "completed", metadata: { files_created: [...] }) when done.')
277
339
  # All agents launched in parallel
@@ -397,6 +459,31 @@ test({module}): backend unit and integration tests
397
459
  - Read `references/smartstack-frontend.md` now — lazy loading, i18n, page structure, CSS variables, EntityLookup (sections 1-6)
398
460
  - Read `references/smartstack-frontend-compliance.md` now — documentation, form testing, compliance gates (sections 7-9)
399
461
 
462
+ ### Pre-flight: Shared component existence check
463
+
464
+ Before generating any page, verify shared components exist:
465
+
466
+ ```
467
+ Glob("src/components/ui/DataTable.*")
468
+ Glob("src/components/ui/EntityCard.*")
469
+ Glob("src/components/ui/PageLoader.*")
470
+ Glob("src/components/ui/EntityLookup.*")
471
+ ```
472
+
473
+ If ANY shared component is MISSING:
474
+ → Log warning: "Shared component {Component} not found — will be generated locally"
475
+ → When invoking Skill("ui-components"), add instruction:
476
+ "MISSING SHARED COMPONENTS: {list}. Generate these locally in src/components/ui/"
477
+
478
+ **EntityLookup special handling (FK fields):**
479
+ If ANY entity has FK Guid fields (e.g., EmployeeId, DepartmentId) AND EntityLookup is missing:
480
+ → Generate EntityLookup FIRST, BEFORE any Create/Edit pages
481
+ → Use the EXACT implementation from `references/smartstack-frontend.md` section 6 (EntityLookup Component Pattern)
482
+ → Critical: response parsing MUST use `(res.data.items || res.data).map(mapOption)` to handle both paginated and array responses
483
+ → Do NOT improvise — copy the reference implementation verbatim
484
+ → Do NOT write a simplified version with `result.data` or `Array.isArray(result.data)` — the reference handles both `PaginatedResult<T>` (`.items`) and raw array responses
485
+ → Verify after generation: the component MUST contain the expression `(res.data.items || res.data).map(mapOption)` — if it does not, replace with the reference version
486
+
400
487
  ### Task Progress
401
488
  TaskUpdate(taskId: layer3_task_id, status: "in_progress")
402
489
  TaskUpdate(taskId: progress_tracker_id,
@@ -519,9 +606,12 @@ This generates:
519
606
 
520
607
  ```
521
608
  IF NOT economy_mode AND entities.length > 1:
609
+ # PHASE A — Parallel: infrastructure frontend (NO page generation)
610
+ # Snipper agents DO NOT have access to the Skill tool, so they CANNOT call /ui-components.
611
+ # Pages MUST be generated by the principal agent in Phase B.
522
612
  For each entity, launch in parallel (single message):
523
613
  Agent(subagent_type='Snipper', model='opus',
524
- prompt='Execute Layer 3 frontend for {EntityName}:
614
+ prompt='Execute Layer 3 INFRASTRUCTURE for {EntityName}:
525
615
  **MANDATORY: Read references/smartstack-frontend.md FIRST**
526
616
  - API client: MCP scaffold_api_client
527
617
  - Routes: MCP scaffold_routes (outputFormat: "applicationRoutes", dryRun: false) → MUST generate navRoutes.generated.ts
@@ -529,15 +619,23 @@ IF NOT economy_mode AND entities.length > 1:
529
619
  → CRITICAL: Route paths MUST include module segment: {module_kebab}/{section_kebab} (e.g., employee-management/employees, NOT just employees)
530
620
  → See references/frontend-route-wiring-app-tsx.md for full patterns
531
621
  → Verify: mcp__smartstack__validate_frontend_routes (scope: "routes")
532
- - Pages: /ui-components skill (ALL 4 types: List, Detail, Create, Edit)
533
622
  - I18n: 4 JSON files (fr, en, it, de) + REGISTER namespace in i18n config
534
- - FORM PAGES: Full pages with own routes (no modals)
535
- - FK FIELDS: EntityLookup for all FK Guid fields
536
- - TABS: Local state only (NEVER navigate())
537
- - FORM TESTS: Co-located .test.tsx for Create and Edit pages
623
+ - DO NOT generate any .tsx page files — pages are handled by the principal agent
538
624
  - Your task ID is {task_id}. Call TaskUpdate(status: "in_progress") before starting.
539
625
  - Call TaskUpdate(status: "completed", metadata: { files_created: [...] }) when done.')
540
- # All agents launched in parallel
626
+ # Wait for all agents to complete
627
+
628
+ # PHASE B — Sequential: pages via /ui-components (principal agent)
629
+ # Snipper agents cannot call Skill() — only the principal agent can.
630
+ For each entity (sequentially):
631
+ **INVOKE Skill("ui-components")** — pass entity context:
632
+ - Entity: {EntityName}, Module: {ModuleName}, App: {AppName}
633
+ - Page types: List, Detail, Create, Edit (+ Dashboard if applicable)
634
+ - "CSS: Use CSS variables ONLY — bg-[var(--bg-card)], text-[var(--text-primary)]"
635
+ - "Forms: Create/Edit are FULL PAGES with own routes (/create, /:id/edit)"
636
+ - "FK FIELDS: EntityLookup for ALL FK Guid fields"
637
+ - "I18n: ALL text uses t('namespace:key', 'Fallback')"
638
+ Generate form tests: co-located .test.tsx for Create and Edit pages
541
639
 
542
640
  ELSE:
543
641
  # Economy mode: Agent principal handles all entities SEQUENTIALLY.
@@ -601,6 +699,11 @@ When delegating to `/ui-components` skill, include explicit instructions:
601
699
  >
602
700
  > **Why this matters:** Without the skill, pages miss CSS variables, DataTable/EntityCard,
603
701
  > memo()/useCallback, responsive mobile-first design, and accessibility patterns.
702
+ >
703
+ > **Agent boundary rule:** Snipper sub-agents DO NOT have access to the Skill tool.
704
+ > Therefore, .tsx page generation MUST NEVER be delegated to Snipper agents.
705
+ > Pages are ALWAYS generated by the principal agent via Skill("ui-components").
706
+ > Snipper agents handle: API clients, routes, wiring, i18n, tests — NOT pages.
604
707
 
605
708
  ### Frontend Tests Inline
606
709
 
@@ -157,7 +157,7 @@ export function $MODULE_PASCALListView({
157
157
  ...(search && { search }),
158
158
  });
159
159
  const result = await api.get<PaginatedResult>(`/api/$module?${params}`);
160
- setData(result);
160
+ setData(result ?? null);
161
161
  } catch (err) {
162
162
  setError(t('common:errors.loadFailed'));
163
163
  console.error('Failed to load $module:', err);
@@ -42,7 +42,7 @@ Generate the interactive HTML document of the business analysis from the JSON an
42
42
 
43
43
  - **NEVER** deploy an empty template — FEATURE_DATA must be injected
44
44
  - **moduleSpecs** MUST have ONE entry per module (empty = BROKEN)
45
- - **Scope keys** MUST be converted: `mustHave` → `vital`, `shouldHave` → `important`, `couldHave` → `optional`, `outOfScope` → `excluded`
45
+ - **Scope keys** MUST be converted: `inScope` → `inscope`, `outOfScope` → `outofscope`
46
46
  - **Wireframe fields** MUST be renamed: `mockupFormat` → `format`, `mockup` → `content`
47
47
  - **Final file** MUST be > 100KB
48
48