@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.
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/skills/apex/references/code-generation.md +1 -1
- package/templates/skills/apex/references/person-extension-pattern.md +23 -2
- package/templates/skills/apex/references/post-checks.md +52 -0
- package/templates/skills/apex/references/smartstack-api.md +111 -0
- package/templates/skills/apex/references/smartstack-frontend.md +25 -2
- package/templates/skills/apex/references/smartstack-layers.md +1 -0
- package/templates/skills/apex/steps/step-03-execute.md +110 -7
- package/templates/skills/application/templates-frontend.md +1 -1
- package/templates/skills/ba-generate-html/SKILL.md +1 -1
- package/templates/skills/ba-generate-html/html/ba-interactive.html +42 -78
- package/templates/skills/ba-generate-html/html/src/partials/cadrage-scope.html +14 -36
- package/templates/skills/ba-generate-html/html/src/partials/decomp-modules.html +0 -8
- package/templates/skills/ba-generate-html/html/src/scripts/01-data-init.js +20 -20
- package/templates/skills/ba-generate-html/html/src/scripts/03-render-cadrage.js +4 -3
- package/templates/skills/ba-generate-html/html/src/scripts/04-render-modules.js +0 -2
- package/templates/skills/ba-generate-html/html/src/scripts/07-render-handoff.js +2 -5
- package/templates/skills/ba-generate-html/html/src/scripts/10-comments.js +1 -1
- package/templates/skills/ba-generate-html/html/src/styles/04-cards.css +2 -4
- package/templates/skills/ba-generate-html/html/src/template.html +14 -44
- package/templates/skills/ba-generate-html/references/data-build.md +4 -9
- package/templates/skills/ba-generate-html/references/data-mapping.md +2 -7
- package/templates/skills/ba-generate-html/references/output-modes.md +1 -1
- package/templates/skills/ba-generate-html/steps/step-02-build-data.md +3 -6
- package/templates/skills/ba-generate-html/steps/step-04-verify.md +2 -2
- package/templates/skills/ba-review/references/review-data-mapping.md +4 -6
- package/templates/skills/ba-review/steps/step-01-apply.md +2 -4
- package/templates/skills/business-analyse/patterns/suggestion-catalog.md +4 -4
- package/templates/skills/business-analyse/questionnaire.md +1 -1
- package/templates/skills/business-analyse/react/schema.md +2 -7
- package/templates/skills/business-analyse/schemas/application-schema.json +2 -9
- package/templates/skills/business-analyse/schemas/project-schema.json +4 -8
- package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +1 -3
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +5 -12
- package/templates/skills/business-analyse/steps/step-02-structure.md +3 -5
- package/templates/skills/dev-start/SKILL.md +242 -0
- package/templates/skills/ui-components/SKILL.md +1 -1
- package/templates/skills/ui-components/patterns/data-table.md +1 -1
package/package.json
CHANGED
|
@@ -335,7 +335,7 @@ public class EmployeeService : IEmployeeService
|
|
|
335
335
|
_db.Employees.Add(entity);
|
|
336
336
|
await _db.SaveChangesAsync(ct);
|
|
337
337
|
|
|
338
|
-
return
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
-
#
|
|
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: `
|
|
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
|
|