@atlashub/smartstack-cli 3.24.0 → 3.25.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 +5 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +40 -9
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/skills/apex/references/smartstack-api.md +143 -0
- package/templates/skills/apex/references/smartstack-frontend.md +20 -0
- package/templates/skills/apex/steps/step-03-execute.md +4 -2
- package/templates/skills/apex/steps/step-04-validate.md +152 -11
- package/templates/skills/efcore/steps/migration/step-02-create.md +14 -1
- package/templates/skills/ralph-loop/references/category-rules.md +26 -2
- package/templates/skills/ralph-loop/references/compact-loop.md +1 -1
- package/templates/skills/ralph-loop/steps/step-02-execute.md +81 -1
- package/templates/skills/validate-feature/steps/step-01-compile.md +4 -1
package/package.json
CHANGED
|
@@ -505,3 +505,146 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
|
|
|
505
505
|
| `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
|
|
506
506
|
| `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
|
|
507
507
|
| FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Critical Anti-Patterns (with code examples)
|
|
512
|
+
|
|
513
|
+
> **These are the most common and dangerous mistakes.** Each one has been observed in production code generation.
|
|
514
|
+
|
|
515
|
+
### Anti-Pattern 1: HasQueryFilter with `!= Guid.Empty` (SECURITY — OWASP A01)
|
|
516
|
+
|
|
517
|
+
The `HasQueryFilter` in EF Core should use **runtime tenant resolution**, NOT a static comparison against `Guid.Empty`.
|
|
518
|
+
|
|
519
|
+
**INCORRECT — Does NOT isolate tenants:**
|
|
520
|
+
```csharp
|
|
521
|
+
// WRONG: This only excludes empty GUIDs — ALL tenant data is still visible to everyone!
|
|
522
|
+
public void Configure(EntityTypeBuilder<MyEntity> builder)
|
|
523
|
+
{
|
|
524
|
+
builder.HasQueryFilter(e => e.TenantId != Guid.Empty);
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**CORRECT — Tenant isolation via service:**
|
|
529
|
+
```csharp
|
|
530
|
+
// CORRECT: In SmartStack, tenant filtering is done in the SERVICE layer, not via HasQueryFilter.
|
|
531
|
+
public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
|
|
532
|
+
{
|
|
533
|
+
var query = _db.MyEntities
|
|
534
|
+
.Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY runtime filter
|
|
535
|
+
.AsNoTracking();
|
|
536
|
+
// ...
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**Why it's wrong:** `HasQueryFilter(e => e.TenantId != Guid.Empty)` is a **static filter** — it only removes records with empty GUIDs. It does NOT restrict data to the current tenant. This is an **OWASP A01 Broken Access Control** vulnerability.
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### Anti-Pattern 2: `List<T>` instead of `PaginatedResult<T>` for GetAll
|
|
545
|
+
|
|
546
|
+
**INCORRECT — No pagination:**
|
|
547
|
+
```csharp
|
|
548
|
+
// WRONG: Returns all records at once — no pagination, no totalCount
|
|
549
|
+
public async Task<List<MyEntityDto>> GetAllAsync(CancellationToken ct)
|
|
550
|
+
{
|
|
551
|
+
return await _db.MyEntities
|
|
552
|
+
.Where(x => x.TenantId == _currentUser.TenantId)
|
|
553
|
+
.Select(x => new MyEntityDto(x.Id, x.Code, x.Name))
|
|
554
|
+
.ToListAsync(ct);
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**CORRECT — Paginated with search:**
|
|
559
|
+
```csharp
|
|
560
|
+
// CORRECT: Returns PaginatedResult<T> with search, page, pageSize
|
|
561
|
+
public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(
|
|
562
|
+
string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
|
|
563
|
+
{
|
|
564
|
+
var query = _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).AsNoTracking();
|
|
565
|
+
if (!string.IsNullOrWhiteSpace(search))
|
|
566
|
+
query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
|
|
567
|
+
var totalCount = await query.CountAsync(ct);
|
|
568
|
+
var items = await query.OrderBy(x => x.Name).Skip((page - 1) * pageSize).Take(pageSize)
|
|
569
|
+
.Select(x => new MyEntityDto(x.Id, x.Code, x.Name, x.CreatedAt)).ToListAsync(ct);
|
|
570
|
+
return new PaginatedResult<MyEntityDto>(items, totalCount, page, pageSize);
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Why it's wrong:** `List<T>` loads ALL records into memory. It also breaks `EntityLookup` which requires `{ items, totalCount }` response format.
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
### Anti-Pattern 3: Missing `IAuditableEntity` on tenant entities
|
|
579
|
+
|
|
580
|
+
**INCORRECT — No audit trail:**
|
|
581
|
+
```csharp
|
|
582
|
+
// WRONG: Tenant entity without IAuditableEntity
|
|
583
|
+
public class MyEntity : BaseEntity, ITenantEntity
|
|
584
|
+
{
|
|
585
|
+
public Guid TenantId { get; private set; }
|
|
586
|
+
public string Code { get; private set; } = null!;
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**CORRECT — Always pair ITenantEntity with IAuditableEntity:**
|
|
591
|
+
```csharp
|
|
592
|
+
public class MyEntity : BaseEntity, ITenantEntity, IAuditableEntity
|
|
593
|
+
{
|
|
594
|
+
public Guid TenantId { get; private set; }
|
|
595
|
+
public string? CreatedBy { get; set; }
|
|
596
|
+
public string? UpdatedBy { get; set; }
|
|
597
|
+
public string Code { get; private set; } = null!;
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Why it's wrong:** Without `IAuditableEntity`, there is no record of who created or modified data. Mandatory for compliance in multi-tenant environments.
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
### Anti-Pattern 4: Code auto-generation with `Count() + 1`
|
|
606
|
+
|
|
607
|
+
**INCORRECT — Race condition:**
|
|
608
|
+
```csharp
|
|
609
|
+
// WRONG: Two concurrent requests get the same count
|
|
610
|
+
var count = await _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).CountAsync(ct);
|
|
611
|
+
return $"ENT{(count + 1):D4}";
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**CORRECT — Use `GenerateNextCodeAsync()` (atomic):**
|
|
615
|
+
```csharp
|
|
616
|
+
var code = await GenerateNextCodeAsync("MyEntity", _currentUser.TenantId, ct);
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Why it's wrong:** `Count() + 1` causes **race conditions** — concurrent requests generate duplicate codes.
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
### Anti-Pattern 5: Missing Update validator
|
|
624
|
+
|
|
625
|
+
**INCORRECT — Only CreateValidator:**
|
|
626
|
+
```csharp
|
|
627
|
+
public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto>
|
|
628
|
+
{
|
|
629
|
+
public CreateMyEntityDtoValidator()
|
|
630
|
+
{
|
|
631
|
+
RuleFor(x => x.Code).NotEmpty().MaximumLength(100);
|
|
632
|
+
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// No UpdateMyEntityDtoValidator exists!
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
**CORRECT — Always create validators in pairs:**
|
|
639
|
+
```csharp
|
|
640
|
+
public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto> { /* ... */ }
|
|
641
|
+
public class UpdateMyEntityDtoValidator : AbstractValidator<UpdateMyEntityDto>
|
|
642
|
+
{
|
|
643
|
+
public UpdateMyEntityDtoValidator()
|
|
644
|
+
{
|
|
645
|
+
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**Why it's wrong:** Without an `UpdateValidator`, the Update endpoint accepts **any data without validation**.
|
|
@@ -58,6 +58,26 @@ element: <EmployeesPage />
|
|
|
58
58
|
<Suspense><EmployeesPage /></Suspense>
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Client App.tsx — Lazy Imports Mandatory
|
|
62
|
+
|
|
63
|
+
> **CRITICAL:** In the client `App.tsx` (where `contextRoutes` are defined), ALL page imports MUST use `React.lazy()`.
|
|
64
|
+
|
|
65
|
+
**CORRECT — Lazy imports in client App.tsx:**
|
|
66
|
+
```tsx
|
|
67
|
+
const ClientsListPage = lazy(() =>
|
|
68
|
+
import('@/pages/Business/HumanResources/Clients/ClientsListPage')
|
|
69
|
+
.then(m => ({ default: m.ClientsListPage }))
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**FORBIDDEN — Static imports in client App.tsx:**
|
|
74
|
+
```tsx
|
|
75
|
+
// WRONG: Static import kills code splitting
|
|
76
|
+
import { ClientsListPage } from '@/pages/Business/HumanResources/Clients/ClientsListPage';
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> **Note:** The `smartstackRoutes.tsx` from the npm package may use static imports internally — this is acceptable for the package. But client `App.tsx` code MUST always use lazy imports for business pages.
|
|
80
|
+
|
|
61
81
|
---
|
|
62
82
|
|
|
63
83
|
## 2. I18n / Translations (react-i18next)
|
|
@@ -45,8 +45,10 @@ For each entity:
|
|
|
45
45
|
```
|
|
46
46
|
1. MCP suggest_migration → get standardized name
|
|
47
47
|
2. dotnet ef migrations add {Name} --project src/{Infra}.csproj --startup-project src/{Api}.csproj -o Persistence/Migrations
|
|
48
|
-
3.
|
|
49
|
-
|
|
48
|
+
3. Cleanup corrupted EF Core artifacts:
|
|
49
|
+
for d in src/*/bin?Debug; do [ -d "$d" ] && rm -rf "$d"; done
|
|
50
|
+
4. dotnet ef database update (if local DB)
|
|
51
|
+
5. dotnet build → MUST PASS
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
**BLOCKING:** If build fails after migration, fix EF configs before proceeding.
|
|
@@ -54,6 +54,9 @@ Verify:
|
|
|
54
54
|
## 4. Build Verification
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
+
# Cleanup corrupted EF Core design-time artifacts (Roslyn BuildHost bug on Windows)
|
|
58
|
+
for d in src/*/bin?Debug; do [ -d "$d" ] && echo "Removing corrupted artifact: $d" && rm -rf "$d"; done
|
|
59
|
+
|
|
57
60
|
# Backend
|
|
58
61
|
dotnet clean && dotnet restore && dotnet build
|
|
59
62
|
|
|
@@ -299,6 +302,17 @@ fi
|
|
|
299
302
|
### POST-CHECK 10: Form pages must have companion test files
|
|
300
303
|
|
|
301
304
|
```bash
|
|
305
|
+
# Minimum requirement: if frontend pages exist, at least 1 test file must be present
|
|
306
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
|
|
307
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
308
|
+
ALL_TESTS=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
|
|
309
|
+
if [ -z "$ALL_TESTS" ]; then
|
|
310
|
+
echo "BLOCKING: No frontend test files found in src/pages/"
|
|
311
|
+
echo "Every form page MUST have a companion .test.tsx file"
|
|
312
|
+
exit 1
|
|
313
|
+
fi
|
|
314
|
+
fi
|
|
315
|
+
|
|
302
316
|
# Every CreatePage and EditPage must have a .test.tsx file
|
|
303
317
|
FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
|
|
304
318
|
if [ -n "$FORM_PAGES" ]; then
|
|
@@ -320,14 +334,18 @@ fi
|
|
|
320
334
|
# Check for FK fields rendered as plain text inputs in form pages
|
|
321
335
|
FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test | grep -v node_modules)
|
|
322
336
|
if [ -n "$FORM_PAGES" ]; then
|
|
323
|
-
# Detect
|
|
324
|
-
|
|
325
|
-
if [ -n "$
|
|
326
|
-
|
|
327
|
-
echo "
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
337
|
+
# Detect FK input fields: <input> with name ending in "Id" (catches inputs with or without explicit type)
|
|
338
|
+
FK_INPUTS=$(grep -Pn '<input[^>]*name=["\x27][a-zA-Z]*Id["\x27]' $FORM_PAGES 2>/dev/null)
|
|
339
|
+
if [ -n "$FK_INPUTS" ]; then
|
|
340
|
+
# Filter out hidden inputs (legitimate for FK submission)
|
|
341
|
+
FK_TEXT_INPUTS=$(echo "$FK_INPUTS" | grep -Pv 'type=["\x27]hidden["\x27]')
|
|
342
|
+
if [ -n "$FK_TEXT_INPUTS" ]; then
|
|
343
|
+
echo "BLOCKING: FK fields rendered as plain text inputs — MUST use EntityLookup component"
|
|
344
|
+
echo "Users cannot type GUIDs manually. Use <EntityLookup /> from @/components/ui/EntityLookup"
|
|
345
|
+
echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
|
|
346
|
+
echo "$FK_TEXT_INPUTS"
|
|
347
|
+
exit 1
|
|
348
|
+
fi
|
|
331
349
|
fi
|
|
332
350
|
|
|
333
351
|
# Check for input placeholders mentioning "ID" or "id" or "Enter...Id"
|
|
@@ -362,9 +380,10 @@ fi
|
|
|
362
380
|
### POST-CHECK 13: No hardcoded Tailwind colors in generated pages
|
|
363
381
|
|
|
364
382
|
```bash
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
383
|
+
# Scan all page and component files directly (works for uncommitted/untracked files, Windows/WSL compatible)
|
|
384
|
+
ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
385
|
+
if [ -n "$ALL_PAGES" ]; then
|
|
386
|
+
HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
|
|
368
387
|
if [ -n "$HARDCODED" ]; then
|
|
369
388
|
echo "WARNING: Pages should use CSS variables instead of hardcoded Tailwind colors"
|
|
370
389
|
echo "Fix: bg-[var(--bg-card)] instead of bg-white, text-[var(--text-primary)] instead of text-gray-900"
|
|
@@ -373,6 +392,123 @@ if [ -n "$NEW_PAGES" ]; then
|
|
|
373
392
|
fi
|
|
374
393
|
```
|
|
375
394
|
|
|
395
|
+
### POST-CHECK 14: Routes seed data must match frontend contextRoutes
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
SEED_ROUTES=$(grep -Poh 'Route\s*=\s*"([^"]+)"' $(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null) 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
|
|
399
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
400
|
+
if [ -n "$APP_TSX" ] && [ -n "$SEED_ROUTES" ]; then
|
|
401
|
+
FRONTEND_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | grep -oP "'[^']+'" | tr -d "'" | sort -u)
|
|
402
|
+
if [ -n "$FRONTEND_PATHS" ]; then
|
|
403
|
+
MISMATCH_FOUND=false
|
|
404
|
+
for SEED_ROUTE in $SEED_ROUTES; do
|
|
405
|
+
DEPTH=$(echo "$SEED_ROUTE" | tr '/' '\n' | grep -c '.')
|
|
406
|
+
if [ "$DEPTH" -lt 3 ]; then continue; fi
|
|
407
|
+
SEED_SUFFIX=$(echo "$SEED_ROUTE" | sed 's|^/[^/]*/||')
|
|
408
|
+
SEED_NORM=$(echo "$SEED_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr -d '-')
|
|
409
|
+
MATCH_FOUND=false
|
|
410
|
+
for FE_PATH in $FRONTEND_PATHS; do
|
|
411
|
+
FE_BASE=$(echo "$FE_PATH" | sed 's|/list$||;s|/new$||;s|/:id.*||;s|/create$||')
|
|
412
|
+
FE_NORM=$(echo "$FE_BASE" | tr '[:upper:]' '[:lower:]' | tr -d '-')
|
|
413
|
+
if [ "$SEED_NORM" = "$FE_NORM" ]; then
|
|
414
|
+
MATCH_FOUND=true
|
|
415
|
+
break
|
|
416
|
+
fi
|
|
417
|
+
done
|
|
418
|
+
if [ "$MATCH_FOUND" = false ]; then
|
|
419
|
+
echo "BLOCKING: Seed data route has no matching frontend route: $SEED_ROUTE"
|
|
420
|
+
MISMATCH_FOUND=true
|
|
421
|
+
fi
|
|
422
|
+
done
|
|
423
|
+
if [ "$MISMATCH_FOUND" = true ]; then
|
|
424
|
+
echo "Fix: Ensure every NavigationSeedData route has a corresponding contextRoutes entry in App.tsx"
|
|
425
|
+
exit 1
|
|
426
|
+
fi
|
|
427
|
+
fi
|
|
428
|
+
fi
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### POST-CHECK 15: HasQueryFilter must not use Guid.Empty (OWASP A01)
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
|
|
435
|
+
if [ -n "$CONFIG_FILES" ]; then
|
|
436
|
+
BAD_FILTERS=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
|
|
437
|
+
if [ -n "$BAD_FILTERS" ]; then
|
|
438
|
+
echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty instead of runtime tenant isolation"
|
|
439
|
+
echo "$BAD_FILTERS"
|
|
440
|
+
echo ""
|
|
441
|
+
echo "Anti-pattern: .HasQueryFilter(e => e.TenantId != Guid.Empty)"
|
|
442
|
+
echo "Fix: Remove HasQueryFilter. Tenant isolation is handled by SmartStack base DbContext"
|
|
443
|
+
exit 1
|
|
444
|
+
fi
|
|
445
|
+
fi
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### POST-CHECK 16: GetAll methods must return PaginatedResult<T>
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
452
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
453
|
+
BAD_RETURNS=$(grep -Pn '(Task<\s*(?:List|IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection)<).*GetAll' $SERVICE_FILES 2>/dev/null)
|
|
454
|
+
if [ -n "$BAD_RETURNS" ]; then
|
|
455
|
+
echo "BLOCKING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
|
|
456
|
+
echo "$BAD_RETURNS"
|
|
457
|
+
echo "Fix: Change return type to Task<PaginatedResult<{Entity}ResponseDto>>"
|
|
458
|
+
exit 1
|
|
459
|
+
fi
|
|
460
|
+
fi
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### POST-CHECK 17: i18n files must contain required structural keys
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
I18N_DIR="src/i18n/locales/fr"
|
|
467
|
+
if [ -d "$I18N_DIR" ]; then
|
|
468
|
+
REQUIRED_KEYS="actions columns empty errors form labels messages validation"
|
|
469
|
+
for JSON_FILE in "$I18N_DIR"/*.json; do
|
|
470
|
+
[ ! -f "$JSON_FILE" ] && continue
|
|
471
|
+
BASENAME=$(basename "$JSON_FILE")
|
|
472
|
+
case "$BASENAME" in common.json|navigation.json) continue;; esac
|
|
473
|
+
for KEY in $REQUIRED_KEYS; do
|
|
474
|
+
if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
|
|
475
|
+
echo "BLOCKING: i18n file missing required key '$KEY': $JSON_FILE"
|
|
476
|
+
echo "Module i18n files MUST contain: $REQUIRED_KEYS"
|
|
477
|
+
exit 1
|
|
478
|
+
fi
|
|
479
|
+
done
|
|
480
|
+
done
|
|
481
|
+
fi
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### POST-CHECK 18: Entities must implement IAuditableEntity + Validators must have Create/Update pairs
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
|
|
488
|
+
if [ -n "$ENTITY_FILES" ]; then
|
|
489
|
+
for f in $ENTITY_FILES; do
|
|
490
|
+
if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
|
|
491
|
+
echo "BLOCKING: Entity implements ITenantEntity but NOT IAuditableEntity: $f"
|
|
492
|
+
echo "Pattern: public class Entity : BaseEntity, ITenantEntity, IAuditableEntity"
|
|
493
|
+
exit 1
|
|
494
|
+
fi
|
|
495
|
+
done
|
|
496
|
+
fi
|
|
497
|
+
CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
|
|
498
|
+
if [ -n "$CREATE_VALIDATORS" ]; then
|
|
499
|
+
for f in $CREATE_VALIDATORS; do
|
|
500
|
+
VALIDATOR_DIR=$(dirname "$f")
|
|
501
|
+
ENTITY_NAME=$(basename "$f" | sed 's/^Create\(.*\)Validator\.cs$/\1/')
|
|
502
|
+
if [ ! -f "$VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs" ]; then
|
|
503
|
+
echo "BLOCKING: Create${ENTITY_NAME}Validator exists but Update${ENTITY_NAME}Validator is missing"
|
|
504
|
+
echo " Found: $f"
|
|
505
|
+
echo " Expected: $VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs"
|
|
506
|
+
exit 1
|
|
507
|
+
fi
|
|
508
|
+
done
|
|
509
|
+
fi
|
|
510
|
+
```
|
|
511
|
+
|
|
376
512
|
**If ANY POST-CHECK fails → fix in step-03, re-validate.**
|
|
377
513
|
|
|
378
514
|
---
|
|
@@ -414,6 +550,11 @@ AC2: {criterion} → PASS / FAIL (evidence: {file:line or test})
|
|
|
414
550
|
| FK fields: EntityLookup, no plain text | PASS / N/A |
|
|
415
551
|
| APIs: search parameter on GetAll | PASS / N/A |
|
|
416
552
|
| CSS variables: no hardcoded colors | PASS / N/A |
|
|
553
|
+
| Routes: seed data vs frontend match | PASS / N/A |
|
|
554
|
+
| HasQueryFilter: no Guid.Empty pattern | PASS / N/A |
|
|
555
|
+
| GetAll: PaginatedResult required | PASS / N/A |
|
|
556
|
+
| I18n: required key structure | PASS / N/A |
|
|
557
|
+
| Entities: IAuditableEntity + validators | PASS / N/A |
|
|
417
558
|
| Acceptance criteria | {X}/{Y} PASS |
|
|
418
559
|
```
|
|
419
560
|
|
|
@@ -107,7 +107,20 @@ dotnet ef migrations add "$MIGRATION_NAME" \
|
|
|
107
107
|
--verbose
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
### 6.
|
|
110
|
+
### 6. Cleanup EF Core Design-Time Artifacts
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# EF Core's Roslyn BuildHost creates corrupted bin folders on Windows (U+F05C instead of backslash)
|
|
114
|
+
# Clean up any bin*Debug folders that are NOT the normal bin/Debug path
|
|
115
|
+
for d in src/*/bin?Debug; do
|
|
116
|
+
if [ -d "$d" ]; then
|
|
117
|
+
echo "Removing corrupted EF Core artifact: $d"
|
|
118
|
+
rm -rf "$d"
|
|
119
|
+
fi
|
|
120
|
+
done
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 7. Verify Creation
|
|
111
124
|
|
|
112
125
|
```bash
|
|
113
126
|
# Check files were created
|
|
@@ -56,8 +56,9 @@ Execution sequence:
|
|
|
56
56
|
> NEVER use `$HOME/.dotnet/tools` alone — on WSL, `$HOME` resolves to `/home/{user}` where .NET SDK is not installed.
|
|
57
57
|
1. Call `mcp__smartstack__suggest_migration` → get standardized name
|
|
58
58
|
2. `dotnet ef migrations add {Name} --project src/{Infra}.csproj --startup-project src/{Api}.csproj -o Persistence/Migrations`
|
|
59
|
-
3.
|
|
60
|
-
4. `dotnet
|
|
59
|
+
3. Cleanup corrupted EF Core artifacts: `for d in src/*/bin?Debug; do [ -d "$d" ] && rm -rf "$d"; done`
|
|
60
|
+
4. `dotnet ef database update --project src/{Infra}.csproj --startup-project src/{Api}.csproj`
|
|
61
|
+
5. `dotnet build --no-restore` → verify build passes
|
|
61
62
|
|
|
62
63
|
**BLOCKING:** If migration fails, DO NOT proceed. Fix the EF configs first.
|
|
63
64
|
|
|
@@ -477,6 +478,29 @@ useState(showCreateModal/editDialog) → navigate to form page, NEVER to
|
|
|
477
478
|
- Coverage >= 80%
|
|
478
479
|
- No `[Fact(Skip = "...")]`
|
|
479
480
|
|
|
481
|
+
**Frontend test completeness (BLOCKING):**
|
|
482
|
+
|
|
483
|
+
After all frontend pages are generated, verify test coverage:
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
# Count page files vs test files
|
|
487
|
+
PAGE_COUNT=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null | wc -l)
|
|
488
|
+
TEST_COUNT=$(find src/pages/ -name "*.test.tsx" 2>/dev/null | wc -l)
|
|
489
|
+
|
|
490
|
+
if [ "$PAGE_COUNT" -gt 0 ] && [ "$TEST_COUNT" -eq 0 ]; then
|
|
491
|
+
echo "BLOCKING: Category D — No .test.tsx files found in src/pages/"
|
|
492
|
+
echo "Every module with pages MUST have at least one test file"
|
|
493
|
+
echo "Pages found: $PAGE_COUNT, Tests found: 0"
|
|
494
|
+
exit 1
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
# Minimum ratio: at least 1 test per 3 pages
|
|
498
|
+
MIN_TESTS=$(( (PAGE_COUNT + 2) / 3 ))
|
|
499
|
+
if [ "$TEST_COUNT" -lt "$MIN_TESTS" ]; then
|
|
500
|
+
echo "WARNING: Low test coverage — $TEST_COUNT tests for $PAGE_COUNT pages (minimum: $MIN_TESTS)"
|
|
501
|
+
fi
|
|
502
|
+
```
|
|
503
|
+
|
|
480
504
|
---
|
|
481
505
|
|
|
482
506
|
## Validation (FINAL — BLOCKING)
|
|
@@ -108,7 +108,7 @@ Batch: {batch.length} [{firstCategory}] → {batch.map(t => `[${t.id}] ${t.descr
|
|
|
108
108
|
|
|
109
109
|
| Category | Action |
|
|
110
110
|
|----------|--------|
|
|
111
|
-
| `infrastructure` with `_migrationMeta` | Migration sequence: `suggest_migration` MCP → `dotnet ef migrations add` → `dotnet ef database update` → `dotnet build` |
|
|
111
|
+
| `infrastructure` with `_migrationMeta` | Migration sequence: `suggest_migration` MCP → `dotnet ef migrations add` → cleanup corrupted artifacts (`for d in src/*/bin?Debug; do [ -d "$d" ] && rm -rf "$d"; done`) → `dotnet ef database update` → `dotnet build` |
|
|
112
112
|
| `infrastructure` with seed data keywords | **MANDATORY:** Read `references/core-seed-data.md` → implement templates → `dotnet build` |
|
|
113
113
|
| `frontend` | MCP-first: `scaffold_api_client` → `scaffold_routes` (outputFormat: clientRoutes) → **wire to App.tsx (detect pattern: `contextRoutes` array OR JSX `<Route>`)** → create pages → `npm run typecheck && npm run lint` |
|
|
114
114
|
| `test` | **Create tests:** `scaffold_tests` MCP → **Run:** `dotnet test --verbosity normal` → **Fix loop:** if fail → fix SOURCE CODE → rebuild → retest → repeat until 100% pass |
|
|
@@ -271,6 +271,17 @@ if [ -n "$LIST_PAGES" ]; then
|
|
|
271
271
|
done
|
|
272
272
|
fi
|
|
273
273
|
|
|
274
|
+
# First check: at least one test file must exist in pages/
|
|
275
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
|
|
276
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
277
|
+
TEST_FILES=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
|
|
278
|
+
if [ -z "$TEST_FILES" ]; then
|
|
279
|
+
echo "BLOCKING: No frontend test files found in src/pages/"
|
|
280
|
+
echo "Every module with pages MUST have at least one .test.tsx file"
|
|
281
|
+
exit 1
|
|
282
|
+
fi
|
|
283
|
+
fi
|
|
284
|
+
|
|
274
285
|
# POST-CHECK: Form pages must have companion test files
|
|
275
286
|
FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
|
|
276
287
|
if [ -n "$FORM_PAGES" ]; then
|
|
@@ -286,7 +297,8 @@ fi
|
|
|
286
297
|
# POST-CHECK: FK fields must NOT be plain text inputs — use EntityLookup
|
|
287
298
|
FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test | grep -v node_modules)
|
|
288
299
|
if [ -n "$FORM_PAGES" ]; then
|
|
289
|
-
|
|
300
|
+
# Match any input with name ending in "Id" (except hidden inputs)
|
|
301
|
+
FK_TEXT_INPUTS=$(grep -Pn '<input[^>]*name=["\x27][a-zA-Z]*Id["\x27]' $FORM_PAGES 2>/dev/null | grep -v 'type=["\x27]hidden["\x27]')
|
|
290
302
|
if [ -n "$FK_TEXT_INPUTS" ]; then
|
|
291
303
|
echo "BLOCKING: FK Guid fields rendered as plain text inputs — MUST use EntityLookup"
|
|
292
304
|
echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
|
|
@@ -313,6 +325,74 @@ if [ -n "$CTRL_FILES" ]; then
|
|
|
313
325
|
fi
|
|
314
326
|
done
|
|
315
327
|
fi
|
|
328
|
+
|
|
329
|
+
# POST-CHECK: Route seed data vs frontend cross-validation
|
|
330
|
+
SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null)
|
|
331
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
332
|
+
if [ -n "$SEED_NAV_FILES" ] && [ -n "$APP_TSX" ]; then
|
|
333
|
+
SEED_ROUTES=$(grep -ohP 'Route\s*=\s*"([^"]+)"' $SEED_NAV_FILES | sed 's/Route\s*=\s*"//' | sed 's/"//' | sort -u)
|
|
334
|
+
CLIENT_PATHS=$(grep -ohP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | sed "s/path:\s*['\"]//;s/['\"]//" | sort -u)
|
|
335
|
+
if [ -n "$SEED_ROUTES" ] && [ -z "$CLIENT_PATHS" ]; then
|
|
336
|
+
echo "WARNING: Seed data has navigation routes but App.tsx has no client routes defined"
|
|
337
|
+
fi
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
# POST-CHECK: HasQueryFilter anti-pattern
|
|
341
|
+
CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
|
|
342
|
+
if [ -n "$CONFIG_FILES" ]; then
|
|
343
|
+
BAD_FILTER=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
|
|
344
|
+
if [ -n "$BAD_FILTER" ]; then
|
|
345
|
+
echo "BLOCKING: HasQueryFilter uses Guid.Empty — must use runtime tenant filter"
|
|
346
|
+
echo "$BAD_FILTER"
|
|
347
|
+
exit 1
|
|
348
|
+
fi
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
# POST-CHECK: PaginatedResult<T> for GetAll
|
|
352
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
353
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
354
|
+
BAD_GETALL=$(grep -Pn 'Task<(List|IEnumerable|IList|ICollection)<' $SERVICE_FILES 2>/dev/null | grep -i "getall")
|
|
355
|
+
if [ -n "$BAD_GETALL" ]; then
|
|
356
|
+
echo "BLOCKING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
|
|
357
|
+
echo "$BAD_GETALL"
|
|
358
|
+
exit 1
|
|
359
|
+
fi
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
# POST-CHECK: i18n required key structure
|
|
363
|
+
FR_I18N_FILES=$(find src/i18n/locales/fr/ -name "*.json" 2>/dev/null)
|
|
364
|
+
if [ -n "$FR_I18N_FILES" ]; then
|
|
365
|
+
for f in $FR_I18N_FILES; do
|
|
366
|
+
BASENAME=$(basename "$f")
|
|
367
|
+
case "$BASENAME" in common.json|navigation.json) continue;; esac
|
|
368
|
+
for KEY in "actions" "labels" "errors"; do
|
|
369
|
+
if ! grep -q "\"$KEY\"" "$f"; then
|
|
370
|
+
echo "WARNING: i18n file missing required key '$KEY': $f"
|
|
371
|
+
fi
|
|
372
|
+
done
|
|
373
|
+
done
|
|
374
|
+
fi
|
|
375
|
+
|
|
376
|
+
# POST-CHECK: IAuditableEntity + Validator pairing
|
|
377
|
+
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/Business/*" -name "*.cs" 2>/dev/null)
|
|
378
|
+
if [ -n "$ENTITY_FILES" ]; then
|
|
379
|
+
for f in $ENTITY_FILES; do
|
|
380
|
+
if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
|
|
381
|
+
echo "WARNING: Entity has ITenantEntity but missing IAuditableEntity: $f"
|
|
382
|
+
fi
|
|
383
|
+
done
|
|
384
|
+
fi
|
|
385
|
+
CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
|
|
386
|
+
if [ -n "$CREATE_VALIDATORS" ]; then
|
|
387
|
+
for CV in $CREATE_VALIDATORS; do
|
|
388
|
+
UV=$(echo "$CV" | sed 's/Create/Update/')
|
|
389
|
+
if [ ! -f "$UV" ]; then
|
|
390
|
+
echo "BLOCKING: CreateValidator without matching UpdateValidator: $CV"
|
|
391
|
+
echo "Expected: $UV"
|
|
392
|
+
exit 1
|
|
393
|
+
fi
|
|
394
|
+
done
|
|
395
|
+
fi
|
|
316
396
|
```
|
|
317
397
|
|
|
318
398
|
**Error resolution cycle (ALL categories):**
|
|
@@ -15,9 +15,12 @@ next_step: steps/step-02-unit-tests.md
|
|
|
15
15
|
ls *.sln 2>/dev/null || find . -maxdepth 2 -name "*.sln" -type f
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
### 2.
|
|
18
|
+
### 2. Cleanup & Build
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
+
# Cleanup corrupted EF Core design-time artifacts (Roslyn BuildHost bug on Windows)
|
|
22
|
+
for d in src/*/bin?Debug; do [ -d "$d" ] && echo "Removing corrupted artifact: $d" && rm -rf "$d"; done
|
|
23
|
+
|
|
21
24
|
dotnet build {SolutionFile} --verbosity minimal
|
|
22
25
|
```
|
|
23
26
|
|