@atlashub/smartstack-cli 4.30.0 → 4.32.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 (43) hide show
  1. package/.documentation/commands.html +952 -116
  2. package/dist/index.js +17 -4
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/skills/apex/references/code-generation.md +1 -1
  6. package/templates/skills/apex/references/parallel-execution.md +18 -5
  7. package/templates/skills/apex/references/person-extension-pattern.md +23 -2
  8. package/templates/skills/apex/references/post-checks.md +52 -0
  9. package/templates/skills/apex/references/smartstack-api.md +111 -0
  10. package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
  11. package/templates/skills/apex/references/smartstack-frontend.md +25 -2
  12. package/templates/skills/apex/references/smartstack-layers.md +1 -0
  13. package/templates/skills/apex/steps/step-03-execute.md +131 -27
  14. package/templates/skills/application/templates-frontend.md +1 -1
  15. package/templates/skills/ba-generate-html/SKILL.md +1 -1
  16. package/templates/skills/ba-generate-html/html/ba-interactive.html +42 -78
  17. package/templates/skills/ba-generate-html/html/src/partials/cadrage-scope.html +14 -36
  18. package/templates/skills/ba-generate-html/html/src/partials/decomp-modules.html +0 -8
  19. package/templates/skills/ba-generate-html/html/src/scripts/01-data-init.js +20 -20
  20. package/templates/skills/ba-generate-html/html/src/scripts/03-render-cadrage.js +4 -3
  21. package/templates/skills/ba-generate-html/html/src/scripts/04-render-modules.js +0 -2
  22. package/templates/skills/ba-generate-html/html/src/scripts/07-render-handoff.js +2 -5
  23. package/templates/skills/ba-generate-html/html/src/scripts/10-comments.js +1 -1
  24. package/templates/skills/ba-generate-html/html/src/styles/04-cards.css +2 -4
  25. package/templates/skills/ba-generate-html/html/src/template.html +14 -44
  26. package/templates/skills/ba-generate-html/references/data-build.md +4 -9
  27. package/templates/skills/ba-generate-html/references/data-mapping.md +2 -7
  28. package/templates/skills/ba-generate-html/references/output-modes.md +1 -1
  29. package/templates/skills/ba-generate-html/steps/step-02-build-data.md +3 -6
  30. package/templates/skills/ba-generate-html/steps/step-04-verify.md +2 -2
  31. package/templates/skills/ba-review/references/review-data-mapping.md +4 -6
  32. package/templates/skills/ba-review/steps/step-01-apply.md +2 -4
  33. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +4 -4
  34. package/templates/skills/business-analyse/questionnaire.md +1 -1
  35. package/templates/skills/business-analyse/react/schema.md +2 -7
  36. package/templates/skills/business-analyse/schemas/application-schema.json +2 -9
  37. package/templates/skills/business-analyse/schemas/project-schema.json +4 -8
  38. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +1 -3
  39. package/templates/skills/business-analyse/steps/step-01-cadrage.md +5 -12
  40. package/templates/skills/business-analyse/steps/step-02-structure.md +3 -5
  41. package/templates/skills/dev-start/SKILL.md +242 -0
  42. package/templates/skills/ui-components/SKILL.md +1 -1
  43. 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.32.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
  ```
@@ -98,14 +98,26 @@ Layer 2 (Backend) — if multiple entities:
98
98
  # Launched in parallel in a single message
99
99
 
100
100
  Layer 3 (Frontend) — if multiple entities:
101
+ # ⛔ AGENT BOUNDARY: Snipper agents do NOT have access to Skill().
102
+ # Pages (.tsx) MUST be generated by the principal agent via Skill("ui-components").
103
+ # Snipper agents handle ONLY: API clients, routes, wiring, i18n — NOT pages.
104
+
105
+ # PHASE A — Parallel: infrastructure (Snipper agents)
101
106
  Agent(subagent_type='Snipper', model='opus',
102
- prompt='Execute Layer 3 frontend for {Entity1}: pages, i18n, routes...')
107
+ prompt='Execute Layer 3 INFRASTRUCTURE for {Entity1}: API client, routes, wiring, i18n.
108
+ DO NOT generate any .tsx page files — pages are handled by the principal agent.')
103
109
  Agent(subagent_type='Snipper', model='opus',
104
- prompt='Execute Layer 3 frontend for {Entity2}: pages, i18n, routes...')
110
+ prompt='Execute Layer 3 INFRASTRUCTURE for {Entity2}: API client, routes, wiring, i18n.
111
+ DO NOT generate any .tsx page files — pages are handled by the principal agent.')
105
112
  # Launched in parallel in a single message
113
+
114
+ # PHASE B — Sequential: pages (principal agent only)
115
+ # After all Snipper agents complete, principal generates pages via Skill("ui-components").
116
+ # See step-03-execute.md Layer 3 HARD RULE for full protocol.
106
117
  ```
107
118
 
108
119
  Each agent has an **isolated scope**: handles one entity end-to-end within the layer.
120
+ **Exception:** Layer 3 pages are NOT delegated — see step-03 HARD RULE.
109
121
 
110
122
  ---
111
123
 
@@ -129,8 +141,9 @@ Each agent has an **isolated scope**: handles one entity end-to-end within the l
129
141
  5. Layer 2: launch Snipper agents per entity (if multi-entity) OR agent principal (if single)
130
142
  6. Build gate: dotnet build → MUST PASS
131
143
  7. Backend tests inline (scaffold + run + fix max 3)
132
- 8. Layer 3: launch Snipper agents per entity (if multi-entity) OR agent principal (if single)
133
- 9. Compliance gate: 5 frontend checks MUST PASS
144
+ 8. Layer 3 Phase A: launch Snipper agents for infra (api-client, routes, i18n) NOT pages
145
+ 9. Layer 3 Phase B: principal agent generates ALL pages via Skill("ui-components") — sequential
146
+ 10. Compliance gate: 6 frontend checks → MUST PASS
134
147
  10. Frontend tests inline (scaffold + run + fix max 3)
135
148
  11. Layer 4 (optional): agent principal executes DevData
136
149
  ```
@@ -151,6 +164,6 @@ There is no idle state to manage — the agent principal simply waits for result
151
164
  | economy_mode = true | NO parallel agents, all sequential |
152
165
  | Single entity (any layer) | NO parallel agents, agent principal handles all |
153
166
  | Multiple entities, Layer 2 | Parallel: one Snipper agent per entity (service + controller) |
154
- | Multiple entities, Layer 3 | Parallel: one Snipper agent per entity (pages + i18n) |
167
+ | Multiple entities, Layer 3 | Parallel: Snipper agents for infra (api-client, routes, i18n) — pages by principal via Skill("ui-components") |
155
168
  | Layer 0, Layer 1, Layer 4 | NO parallel agents (sequential by nature) |
156
169
  | Analysis phase (step-01) | Parallel: 2-3 Explore agents (backend + frontend + context) |
@@ -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
@@ -591,4 +591,26 @@ if [ -n "$PAGE_FILES" ]; then
591
591
  fi
592
592
  ```
593
593
 
594
- > **ALL 5 gates MUST pass before frontend commit.** When delegating to `/ui-components` skill, include explicit instructions: CSS variables only, forms as full pages, i18n with namespace + fallback.
594
+ ### Gate 6: SmartStack Components (no raw HTML tables)
595
+
596
+ ```bash
597
+ PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v "\.test\." | grep -v node_modules)
598
+ if [ -n "$PAGE_FILES" ]; then
599
+ FAIL=false
600
+ RAW_TABLE=$(grep -Pn '<table[> ]|<tr[> ]|<td[> ]|<th[> ]' $PAGE_FILES 2>/dev/null)
601
+ if [ -n "$RAW_TABLE" ]; then
602
+ echo "BLOCKING: Raw HTML <table>/<tr>/<td> found — pages MUST use DataTable component (invoke /ui-components)"
603
+ echo "$RAW_TABLE"
604
+ FAIL=true
605
+ fi
606
+ RAW_CSS=$(grep -Pn 'style=\{?\{|style="[^"]*"' $PAGE_FILES 2>/dev/null | grep -v "className")
607
+ if [ -n "$RAW_CSS" ]; then
608
+ echo "BLOCKING: Inline CSS style= found — pages MUST use Tailwind + CSS variables (invoke /ui-components)"
609
+ echo "$RAW_CSS"
610
+ FAIL=true
611
+ fi
612
+ if [ "$FAIL" = false ]; then echo "PASS: SmartStack components (no raw HTML)"; fi
613
+ fi
614
+ ```
615
+
616
+ > **ALL 6 gates MUST pass before frontend commit.** When delegating to `/ui-components` skill, include explicit instructions: CSS variables only, forms as full pages, i18n with namespace + fallback.
@@ -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
@@ -392,11 +454,59 @@ test({module}): backend unit and integration tests
392
454
 
393
455
  ## Layer 3 — Frontend (Pages + I18n + Documentation)
394
456
 
457
+ ### ⛔ HARD RULE — /ui-components is NON-NEGOTIABLE (read BEFORE any Layer 3 action)
458
+
459
+ > **VIOLATION CHECK:** If ANY .tsx page file was created by Write tool WITHOUT
460
+ > a prior Skill("ui-components") call in this execution, the frontend layer is INVALID.
461
+ >
462
+ > **You MUST NOT:**
463
+ > - Generate .tsx page code in Agent prompts and dispatch to Snipper agents
464
+ > - Write page content directly via the Write tool
465
+ > - Copy-paste page templates from your knowledge
466
+ >
467
+ > **You MUST:**
468
+ > - Call Skill("ui-components") which loads the style guide, responsive guidelines,
469
+ > accessibility rules, and pattern files (entity-card, data-table, dashboard-chart, grid-layout, kanban)
470
+ > - Let /ui-components generate all pages with correct conventions
471
+ >
472
+ > **Why this matters:** Without the skill, pages miss CSS variables, DataTable/EntityCard,
473
+ > memo()/useCallback, responsive mobile-first design, and accessibility patterns.
474
+ >
475
+ > **Agent boundary rule:** Snipper sub-agents DO NOT have access to the Skill tool.
476
+ > Therefore, .tsx page generation MUST NEVER be delegated to Snipper agents.
477
+ > Pages are ALWAYS generated by the principal agent via Skill("ui-components").
478
+ > Snipper agents handle: API clients, routes, wiring, i18n, tests — NOT pages.
479
+
395
480
  ### Load Frontend References (deferred from top of step)
396
481
 
397
482
  - Read `references/smartstack-frontend.md` now — lazy loading, i18n, page structure, CSS variables, EntityLookup (sections 1-6)
398
483
  - Read `references/smartstack-frontend-compliance.md` now — documentation, form testing, compliance gates (sections 7-9)
399
484
 
485
+ ### Pre-flight: Shared component existence check
486
+
487
+ Before generating any page, verify shared components exist:
488
+
489
+ ```
490
+ Glob("src/components/ui/DataTable.*")
491
+ Glob("src/components/ui/EntityCard.*")
492
+ Glob("src/components/ui/PageLoader.*")
493
+ Glob("src/components/ui/EntityLookup.*")
494
+ ```
495
+
496
+ If ANY shared component is MISSING:
497
+ → Log warning: "Shared component {Component} not found — will be generated locally"
498
+ → When invoking Skill("ui-components"), add instruction:
499
+ "MISSING SHARED COMPONENTS: {list}. Generate these locally in src/components/ui/"
500
+
501
+ **EntityLookup special handling (FK fields):**
502
+ If ANY entity has FK Guid fields (e.g., EmployeeId, DepartmentId) AND EntityLookup is missing:
503
+ → Generate EntityLookup FIRST, BEFORE any Create/Edit pages
504
+ → Use the EXACT implementation from `references/smartstack-frontend.md` section 6 (EntityLookup Component Pattern)
505
+ → Critical: response parsing MUST use `(res.data.items || res.data).map(mapOption)` to handle both paginated and array responses
506
+ → Do NOT improvise — copy the reference implementation verbatim
507
+ → 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
508
+ → 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
509
+
400
510
  ### Task Progress
401
511
  TaskUpdate(taskId: layer3_task_id, status: "in_progress")
402
512
  TaskUpdate(taskId: progress_tracker_id,
@@ -519,9 +629,12 @@ This generates:
519
629
 
520
630
  ```
521
631
  IF NOT economy_mode AND entities.length > 1:
632
+ # PHASE A — Parallel: infrastructure frontend (NO page generation)
633
+ # Snipper agents DO NOT have access to the Skill tool, so they CANNOT call /ui-components.
634
+ # Pages MUST be generated by the principal agent in Phase B.
522
635
  For each entity, launch in parallel (single message):
523
636
  Agent(subagent_type='Snipper', model='opus',
524
- prompt='Execute Layer 3 frontend for {EntityName}:
637
+ prompt='Execute Layer 3 INFRASTRUCTURE for {EntityName}:
525
638
  **MANDATORY: Read references/smartstack-frontend.md FIRST**
526
639
  - API client: MCP scaffold_api_client
527
640
  - Routes: MCP scaffold_routes (outputFormat: "applicationRoutes", dryRun: false) → MUST generate navRoutes.generated.ts
@@ -529,15 +642,23 @@ IF NOT economy_mode AND entities.length > 1:
529
642
  → CRITICAL: Route paths MUST include module segment: {module_kebab}/{section_kebab} (e.g., employee-management/employees, NOT just employees)
530
643
  → See references/frontend-route-wiring-app-tsx.md for full patterns
531
644
  → Verify: mcp__smartstack__validate_frontend_routes (scope: "routes")
532
- - Pages: /ui-components skill (ALL 4 types: List, Detail, Create, Edit)
533
645
  - 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
646
+ - DO NOT generate any .tsx page files — pages are handled by the principal agent
538
647
  - Your task ID is {task_id}. Call TaskUpdate(status: "in_progress") before starting.
539
648
  - Call TaskUpdate(status: "completed", metadata: { files_created: [...] }) when done.')
540
- # All agents launched in parallel
649
+ # Wait for all agents to complete
650
+
651
+ # PHASE B — Sequential: pages via /ui-components (principal agent)
652
+ # Snipper agents cannot call Skill() — only the principal agent can.
653
+ For each entity (sequentially):
654
+ **INVOKE Skill("ui-components")** — pass entity context:
655
+ - Entity: {EntityName}, Module: {ModuleName}, App: {AppName}
656
+ - Page types: List, Detail, Create, Edit (+ Dashboard if applicable)
657
+ - "CSS: Use CSS variables ONLY — bg-[var(--bg-card)], text-[var(--text-primary)]"
658
+ - "Forms: Create/Edit are FULL PAGES with own routes (/create, /:id/edit)"
659
+ - "FK FIELDS: EntityLookup for ALL FK Guid fields"
660
+ - "I18n: ALL text uses t('namespace:key', 'Fallback')"
661
+ Generate form tests: co-located .test.tsx for Create and Edit pages
541
662
 
542
663
  ELSE:
543
664
  # Economy mode: Agent principal handles all entities SEQUENTIALLY.
@@ -570,38 +691,21 @@ When launching agents for multi-entity layers:
570
691
 
571
692
  ### Frontend Compliance Gate
572
693
 
573
- > See `references/smartstack-frontend.md` section 9 "Compliance Gates" for all 5 required checks:
694
+ > See `references/smartstack-frontend-compliance.md` section 9 "Compliance Gates" for all 6 required checks:
574
695
  > 1. CSS Variables (theme system)
575
696
  > 2. Forms as Pages (zero modals/drawers/slide-overs)
576
697
  > 3. I18n File Structure (4 languages, separate JSON files)
577
698
  > 4. Lazy Loading (React.lazy() — no static imports)
578
699
  > 5. useTranslation in Pages (all text translated)
700
+ > 6. SmartStack Components (no raw HTML tables — DataTable/EntityCard required)
579
701
 
580
- Do not commit frontend changes until all 5 gates pass.
702
+ Do not commit frontend changes until all 6 gates pass.
581
703
 
582
704
  When delegating to `/ui-components` skill, include explicit instructions:
583
705
  - "CSS: Use CSS variables ONLY — `bg-[var(--bg-card)]`, `text-[var(--text-primary)]`."
584
706
  - "Forms: Create/Edit forms are FULL PAGES with own routes (e.g., `/create`, `/:id/edit`)."
585
707
  - "I18n: ALL text must use `t('namespace:key', 'Fallback')`. Generate JSON files in `src/i18n/locales/`."
586
708
 
587
- ### HARD RULE — /ui-components is NON-NEGOTIABLE
588
-
589
- > **VIOLATION CHECK:** If ANY .tsx page file was created by Write tool WITHOUT
590
- > a prior Skill("ui-components") call in this execution, the frontend layer is INVALID.
591
- >
592
- > **You MUST NOT:**
593
- > - Generate .tsx page code in Agent prompts and dispatch to Snipper agents
594
- > - Write page content directly via the Write tool
595
- > - Copy-paste page templates from your knowledge
596
- >
597
- > **You MUST:**
598
- > - Call Skill("ui-components") which loads the style guide, responsive guidelines,
599
- > accessibility rules, and pattern files (entity-card, data-table, dashboard-chart, grid-layout, kanban)
600
- > - Let /ui-components generate all pages with correct conventions
601
- >
602
- > **Why this matters:** Without the skill, pages miss CSS variables, DataTable/EntityCard,
603
- > memo()/useCallback, responsive mobile-first design, and accessibility patterns.
604
-
605
709
  ### Frontend Tests Inline
606
710
 
607
711
  > **Tests are scaffolded and run WITHIN Layer 3, not deferred to step-07.**
@@ -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