@atlashub/smartstack-cli 3.24.0 → 3.26.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 (33) hide show
  1. package/dist/index.js +5 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +51 -14
  4. package/dist/mcp-entry.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/skills/apex/SKILL.md +26 -5
  7. package/templates/skills/apex/_shared.md +3 -3
  8. package/templates/skills/apex/references/agent-teams-protocol.md +8 -8
  9. package/templates/skills/apex/references/challenge-questions.md +165 -0
  10. package/templates/skills/apex/references/post-checks.md +457 -0
  11. package/templates/skills/apex/references/smartstack-api.md +234 -14
  12. package/templates/skills/apex/references/smartstack-frontend.md +20 -0
  13. package/templates/skills/apex/references/smartstack-layers.md +16 -4
  14. package/templates/skills/apex/steps/step-00-init.md +84 -56
  15. package/templates/skills/apex/steps/step-01-analyze.md +73 -87
  16. package/templates/skills/apex/steps/step-03-execute.md +6 -4
  17. package/templates/skills/apex/steps/step-04-examine.md +198 -0
  18. package/templates/skills/apex/steps/{step-05-examine.md → step-05-deep-review.md} +6 -6
  19. package/templates/skills/apex/steps/step-06-resolve.md +2 -2
  20. package/templates/skills/business-analyse/SKILL.md +28 -0
  21. package/templates/skills/business-analyse/references/agent-module-prompt.md +255 -0
  22. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +26 -10
  23. package/templates/skills/business-analyse/references/team-orchestration.md +437 -0
  24. package/templates/skills/business-analyse/steps/step-02-decomposition.md +31 -4
  25. package/templates/skills/business-analyse/steps/step-03a1-setup.md +21 -0
  26. package/templates/skills/business-analyse/steps/step-03d-validate.md +84 -0
  27. package/templates/skills/efcore/steps/migration/step-02-create.md +14 -1
  28. package/templates/skills/ralph-loop/references/category-rules.md +26 -2
  29. package/templates/skills/ralph-loop/references/compact-loop.md +1 -1
  30. package/templates/skills/ralph-loop/references/core-seed-data.md +45 -10
  31. package/templates/skills/ralph-loop/steps/step-02-execute.md +128 -1
  32. package/templates/skills/validate-feature/steps/step-01-compile.md +4 -1
  33. package/templates/skills/apex/steps/step-04-validate.md +0 -448
@@ -202,12 +202,13 @@ public class {Name}Configuration : IEntityTypeConfiguration<{Name}>
202
202
 
203
203
  ## Service Pattern (tenant-scoped, MANDATORY)
204
204
 
205
- > **CRITICAL:** ALL services MUST inject `ICurrentUser` and filter by `TenantId`. Missing TenantId = OWASP A01 vulnerability.
205
+ > **CRITICAL:** ALL services MUST inject `ICurrentUserService` + `ICurrentTenantService` and filter by `TenantId`. Missing TenantId = OWASP A01 vulnerability.
206
206
 
207
207
  ```csharp
208
208
  using Microsoft.EntityFrameworkCore;
209
209
  using Microsoft.Extensions.Logging;
210
210
  using SmartStack.Application.Common.Interfaces.Identity;
211
+ using SmartStack.Application.Common.Interfaces.Tenants;
211
212
  using SmartStack.Application.Common.Interfaces.Persistence;
212
213
 
213
214
  namespace {ProjectName}.Infrastructure.Services.{Context}.{App}.{Module};
@@ -215,16 +216,19 @@ namespace {ProjectName}.Infrastructure.Services.{Context}.{App}.{Module};
215
216
  public class {Name}Service : I{Name}Service
216
217
  {
217
218
  private readonly IExtensionsDbContext _db;
218
- private readonly ICurrentUser _currentUser;
219
+ private readonly ICurrentUserService _currentUser;
220
+ private readonly ICurrentTenantService _currentTenant;
219
221
  private readonly ILogger<{Name}Service> _logger;
220
222
 
221
223
  public {Name}Service(
222
224
  IExtensionsDbContext db,
223
- ICurrentUser currentUser,
225
+ ICurrentUserService currentUser,
226
+ ICurrentTenantService currentTenant,
224
227
  ILogger<{Name}Service> logger)
225
228
  {
226
229
  _db = db;
227
230
  _currentUser = currentUser;
231
+ _currentTenant = currentTenant;
228
232
  _logger = logger;
229
233
  }
230
234
 
@@ -234,8 +238,12 @@ public class {Name}Service : I{Name}Service
234
238
  int pageSize = 20,
235
239
  CancellationToken ct = default)
236
240
  {
241
+ // MANDATORY guard — throws 401 if no tenant context (e.g., missing X-Tenant-Slug header)
242
+ var tenantId = _currentTenant.TenantId
243
+ ?? throw new UnauthorizedAccessException("Tenant context is required");
244
+
237
245
  var query = _db.{Name}s
238
- .Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY tenant filter
246
+ .Where(x => x.TenantId == tenantId) // MANDATORY tenant filter
239
247
  .AsNoTracking();
240
248
 
241
249
  // Search filter — enables EntityLookup on frontend
@@ -259,8 +267,11 @@ public class {Name}Service : I{Name}Service
259
267
 
260
268
  public async Task<{Name}ResponseDto?> GetByIdAsync(Guid id, CancellationToken ct)
261
269
  {
270
+ var tenantId = _currentTenant.TenantId
271
+ ?? throw new UnauthorizedAccessException("Tenant context is required");
272
+
262
273
  return await _db.{Name}s
263
- .Where(x => x.Id == id && x.TenantId == _currentUser.TenantId) // MANDATORY
274
+ .Where(x => x.Id == id && x.TenantId == tenantId) // MANDATORY
264
275
  .AsNoTracking()
265
276
  .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
266
277
  .FirstOrDefaultAsync(ct);
@@ -268,8 +279,11 @@ public class {Name}Service : I{Name}Service
268
279
 
269
280
  public async Task<{Name}ResponseDto> CreateAsync(Create{Name}Dto dto, CancellationToken ct)
270
281
  {
282
+ var tenantId = _currentTenant.TenantId
283
+ ?? throw new UnauthorizedAccessException("Tenant context is required");
284
+
271
285
  var entity = {Name}.Create(
272
- tenantId: _currentUser.TenantId, // MANDATORY — never Guid.Empty
286
+ tenantId: tenantId, // MANDATORY — never Guid.Empty
273
287
  code: dto.Code,
274
288
  name: dto.Name);
275
289
 
@@ -279,15 +293,18 @@ public class {Name}Service : I{Name}Service
279
293
  await _db.SaveChangesAsync(ct);
280
294
 
281
295
  _logger.LogInformation("Created {Entity} {Id} for tenant {TenantId}",
282
- nameof({Name}), entity.Id, _currentUser.TenantId);
296
+ nameof({Name}), entity.Id, tenantId);
283
297
 
284
298
  return new {Name}ResponseDto(entity.Id, entity.Code, entity.Name, entity.CreatedAt);
285
299
  }
286
300
 
287
301
  public async Task DeleteAsync(Guid id, CancellationToken ct)
288
302
  {
303
+ var tenantId = _currentTenant.TenantId
304
+ ?? throw new UnauthorizedAccessException("Tenant context is required");
305
+
289
306
  var entity = await _db.{Name}s
290
- .FirstOrDefaultAsync(x => x.Id == id && x.TenantId == _currentUser.TenantId, ct)
307
+ .FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, ct)
291
308
  ?? throw new KeyNotFoundException($"{Name} {id} not found");
292
309
 
293
310
  _db.{Name}s.Remove(entity);
@@ -296,14 +313,24 @@ public class {Name}Service : I{Name}Service
296
313
  }
297
314
  ```
298
315
 
299
- **Key interfaces:**
300
- - `ICurrentUser` (from `SmartStack.Application.Common.Interfaces.Identity`): provides `TenantId`, `UserId`, `Email`
316
+ **Key interfaces (from SmartStack NuGet package):**
317
+ - `ICurrentUserService` (from `SmartStack.Application.Common.Interfaces.Identity`): provides `UserId` (Guid?), `Email` (string?), `IsAuthenticated` (bool)
318
+ - `ICurrentTenantService` (from `SmartStack.Application.Common.Interfaces.Tenants`): provides `TenantId` (Guid?), `HasTenant` (bool), `TenantSlug` (string?)
301
319
  - `IExtensionsDbContext` (for client extensions) or `ICoreDbContext` (for platform)
302
320
 
321
+ **MANDATORY guard clause (first line of every method):**
322
+ ```csharp
323
+ var tenantId = _currentTenant.TenantId
324
+ ?? throw new UnauthorizedAccessException("Tenant context is required");
325
+ ```
326
+ This converts a null TenantId into a clean 401 response via `GlobalExceptionHandlerMiddleware`.
327
+
303
328
  **FORBIDDEN in services:**
304
- - `tenantId: Guid.Empty` — always use `_currentUser.TenantId`
305
- - Queries WITHOUT `.Where(x => x.TenantId == _currentUser.TenantId)` data leak
329
+ - `_currentTenant.TenantId!.Value` — throws `InvalidOperationException` (500) instead of clean 401
330
+ - `tenantId: Guid.Empty` always use validated tenantId from guard clause
331
+ - Queries WITHOUT `.Where(x => x.TenantId == tenantId)` — data leak
306
332
  - Missing `ILogger<T>` — undiagnosable in production
333
+ - Using `ICurrentUser` (does NOT exist) — use `ICurrentUserService` + `ICurrentTenantService`
307
334
 
308
335
  ---
309
336
 
@@ -405,14 +432,25 @@ private static string ToKebabCase(string value)
405
432
 
406
433
  ### SeedConstants Pattern
407
434
 
435
+ > **CRITICAL — NavigationContext IDs are NEVER generated.**
436
+ > Contexts (`business`, `platform`, `personal`) are pre-seeded by SmartStack core with hardcoded GUIDs.
437
+ > You MUST query the context by code at runtime in the SeedDataProvider:
438
+ > `var ctx = await db.NavigationContexts.FirstOrDefaultAsync(c => c.Code == "business", ct);`
439
+ > **FORBIDDEN:** `DeterministicGuid("nav:business")` or any ContextId constant in SeedConstants.
440
+
408
441
  ```csharp
409
442
  public static class SeedConstants
410
443
  {
411
444
  // Deterministic GUIDs (SHA256-based, reproducible across environments)
445
+ // NOTE: Application/Module/Section/Resource IDs are deterministic.
446
+ // Context IDs are NOT — they are pre-seeded by SmartStack core.
412
447
  public static readonly Guid ApplicationId = DeterministicGuid("nav:business.humanresources");
413
448
  public static readonly Guid ModuleId = DeterministicGuid("nav:business.humanresources.employees");
414
449
  public static readonly Guid SectionId = DeterministicGuid("nav:business.humanresources.employees.list");
415
450
 
451
+ // FORBIDDEN — Context IDs are NOT deterministic, they come from SmartStack core:
452
+ // public static readonly Guid BusinessContextId = DeterministicGuid("nav:business"); // WRONG!
453
+
416
454
  private static Guid DeterministicGuid(string input)
417
455
  {
418
456
  var hash = System.Security.Cryptography.SHA256.HashData(
@@ -429,6 +467,11 @@ public static class SeedConstants
429
467
  ### Navigation Seed Data Example
430
468
 
431
469
  ```csharp
470
+ // CRITICAL: Look up the context by code — NEVER use a hardcoded/deterministic GUID
471
+ var businessCtx = await db.NavigationContexts
472
+ .FirstOrDefaultAsync(c => c.Code == "business", ct);
473
+ if (businessCtx == null) return; // Context not yet seeded by SmartStack core
474
+
432
475
  // Application: /business/human-resources
433
476
  var app = NavigationApplication.Create(
434
477
  businessCtx.Id, "humanresources", "Human Resources", "HR Management",
@@ -458,6 +501,7 @@ var section = NavigationSection.Create(
458
501
  | `"humanresources"` as route | Must be `"/business/human-resources"` (full path, kebab-case) |
459
502
  | `"employees"` as route | Must be `"/business/human-resources/employees"` (includes parent) |
460
503
  | `Guid.NewGuid()` in seed data | Must use deterministic GUIDs (SHA256) |
504
+ | `DeterministicGuid("nav:business")` for ContextId | Context IDs are pre-seeded by SmartStack core — look up by code |
461
505
  | Missing translations | Must have 4 languages: fr, en, it, de |
462
506
  | Missing NavigationApplicationSeedData | Menu invisible without Application level |
463
507
 
@@ -498,10 +542,186 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
498
542
  | `[Route] + [NavRoute]` | Only `[NavRoute]` needed (resolves route from DB) |
499
543
  | `SmartStack.Domain.Common.Interfaces` | Wrong — interfaces are in `SmartStack.Domain.Common` directly |
500
544
  | `[Authorize]` without `[RequirePermission]` | No RBAC enforcement — always use `[RequirePermission]` |
501
- | `tenantId: Guid.Empty` in services | OWASP A01 — always use `_currentUser.TenantId` |
502
- | Service without `ICurrentUser` | All tenant data leaks — inject `ICurrentUser` |
545
+ | `tenantId: Guid.Empty` in services | OWASP A01 — always use validated `_currentTenant.TenantId` |
546
+ | Service without `ICurrentTenantService` | All tenant data leaks — inject `ICurrentTenantService` |
547
+ | `ICurrentUser` in service code | Does NOT exist — use `ICurrentUserService` + `ICurrentTenantService` |
548
+ | `_currentTenant.TenantId!.Value` | Crashes with 500 — use `?? throw new UnauthorizedAccessException(...)` |
503
549
  | Route `"humanresources"` in seed data | Must be full path `"/business/human-resources"` |
504
550
  | Route without leading `/` | All routes must start with `/` |
505
551
  | `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
506
552
  | `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
507
553
  | FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
554
+
555
+ ---
556
+
557
+ ## Critical Anti-Patterns (with code examples)
558
+
559
+ > **These are the most common and dangerous mistakes.** Each one has been observed in production code generation.
560
+
561
+ ### Anti-Pattern 1: HasQueryFilter with `!= Guid.Empty` (SECURITY — OWASP A01)
562
+
563
+ The `HasQueryFilter` in EF Core should use **runtime tenant resolution**, NOT a static comparison against `Guid.Empty`.
564
+
565
+ **INCORRECT — Does NOT isolate tenants:**
566
+ ```csharp
567
+ // WRONG: This only excludes empty GUIDs — ALL tenant data is still visible to everyone!
568
+ public void Configure(EntityTypeBuilder<MyEntity> builder)
569
+ {
570
+ builder.HasQueryFilter(e => e.TenantId != Guid.Empty);
571
+ }
572
+ ```
573
+
574
+ **CORRECT — Tenant isolation via service:**
575
+ ```csharp
576
+ // CORRECT: In SmartStack, tenant filtering is done in the SERVICE layer, not via HasQueryFilter.
577
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
578
+ {
579
+ var query = _db.MyEntities
580
+ .Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY runtime filter
581
+ .AsNoTracking();
582
+ // ...
583
+ }
584
+ ```
585
+
586
+ **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.
587
+
588
+ ---
589
+
590
+ ### Anti-Pattern 2: `List<T>` instead of `PaginatedResult<T>` for GetAll
591
+
592
+ **INCORRECT — No pagination:**
593
+ ```csharp
594
+ // WRONG: Returns all records at once — no pagination, no totalCount
595
+ public async Task<List<MyEntityDto>> GetAllAsync(CancellationToken ct)
596
+ {
597
+ return await _db.MyEntities
598
+ .Where(x => x.TenantId == _currentUser.TenantId)
599
+ .Select(x => new MyEntityDto(x.Id, x.Code, x.Name))
600
+ .ToListAsync(ct);
601
+ }
602
+ ```
603
+
604
+ **CORRECT — Paginated with search:**
605
+ ```csharp
606
+ // CORRECT: Returns PaginatedResult<T> with search, page, pageSize
607
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(
608
+ string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
609
+ {
610
+ var query = _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).AsNoTracking();
611
+ if (!string.IsNullOrWhiteSpace(search))
612
+ query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
613
+ var totalCount = await query.CountAsync(ct);
614
+ var items = await query.OrderBy(x => x.Name).Skip((page - 1) * pageSize).Take(pageSize)
615
+ .Select(x => new MyEntityDto(x.Id, x.Code, x.Name, x.CreatedAt)).ToListAsync(ct);
616
+ return new PaginatedResult<MyEntityDto>(items, totalCount, page, pageSize);
617
+ }
618
+ ```
619
+
620
+ **Why it's wrong:** `List<T>` loads ALL records into memory. It also breaks `EntityLookup` which requires `{ items, totalCount }` response format.
621
+
622
+ ---
623
+
624
+ ### Anti-Pattern 3: Missing `IAuditableEntity` on tenant entities
625
+
626
+ **INCORRECT — No audit trail:**
627
+ ```csharp
628
+ // WRONG: Tenant entity without IAuditableEntity
629
+ public class MyEntity : BaseEntity, ITenantEntity
630
+ {
631
+ public Guid TenantId { get; private set; }
632
+ public string Code { get; private set; } = null!;
633
+ }
634
+ ```
635
+
636
+ **CORRECT — Always pair ITenantEntity with IAuditableEntity:**
637
+ ```csharp
638
+ public class MyEntity : BaseEntity, ITenantEntity, IAuditableEntity
639
+ {
640
+ public Guid TenantId { get; private set; }
641
+ public string? CreatedBy { get; set; }
642
+ public string? UpdatedBy { get; set; }
643
+ public string Code { get; private set; } = null!;
644
+ }
645
+ ```
646
+
647
+ **Why it's wrong:** Without `IAuditableEntity`, there is no record of who created or modified data. Mandatory for compliance in multi-tenant environments.
648
+
649
+ ---
650
+
651
+ ### Anti-Pattern 4: Code auto-generation with `Count() + 1`
652
+
653
+ **INCORRECT — Race condition:**
654
+ ```csharp
655
+ // WRONG: Two concurrent requests get the same count
656
+ var count = await _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).CountAsync(ct);
657
+ return $"ENT{(count + 1):D4}";
658
+ ```
659
+
660
+ **CORRECT — Use `GenerateNextCodeAsync()` (atomic):**
661
+ ```csharp
662
+ var code = await GenerateNextCodeAsync("MyEntity", _currentUser.TenantId, ct);
663
+ ```
664
+
665
+ **Why it's wrong:** `Count() + 1` causes **race conditions** — concurrent requests generate duplicate codes.
666
+
667
+ ---
668
+
669
+ ### Anti-Pattern 5: Missing Update validator
670
+
671
+ **INCORRECT — Only CreateValidator:**
672
+ ```csharp
673
+ public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto>
674
+ {
675
+ public CreateMyEntityDtoValidator()
676
+ {
677
+ RuleFor(x => x.Code).NotEmpty().MaximumLength(100);
678
+ RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
679
+ }
680
+ }
681
+ // No UpdateMyEntityDtoValidator exists!
682
+ ```
683
+
684
+ **CORRECT — Always create validators in pairs:**
685
+ ```csharp
686
+ public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto> { /* ... */ }
687
+ public class UpdateMyEntityDtoValidator : AbstractValidator<UpdateMyEntityDto>
688
+ {
689
+ public UpdateMyEntityDtoValidator()
690
+ {
691
+ RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
692
+ }
693
+ }
694
+ ```
695
+
696
+ **Why it's wrong:** Without an `UpdateValidator`, the Update endpoint accepts **any data without validation**.
697
+
698
+ ---
699
+
700
+ ### Anti-Pattern 6: `TenantId!.Value` null-forgiving operator (RUNTIME CRASH)
701
+
702
+ The `!` (null-forgiving) operator followed by `.Value` on a `Guid?` suppresses compiler warnings but **throws `InvalidOperationException` at runtime** when TenantId is null.
703
+
704
+ **INCORRECT — Crashes with 500 Internal Server Error:**
705
+ ```csharp
706
+ // WRONG: Throws InvalidOperationException("Nullable object must have a value") → 500 error
707
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
708
+ {
709
+ var tenantId = _currentTenant.TenantId!.Value; // CRASH if no tenant context
710
+ var query = _db.MyEntities.Where(x => x.TenantId == tenantId);
711
+ // ...
712
+ }
713
+ ```
714
+
715
+ **CORRECT — Clean 401 via GlobalExceptionHandlerMiddleware:**
716
+ ```csharp
717
+ // CORRECT: Throws UnauthorizedAccessException → middleware converts to 401
718
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
719
+ {
720
+ var tenantId = _currentTenant.TenantId
721
+ ?? throw new UnauthorizedAccessException("Tenant context is required");
722
+ var query = _db.MyEntities.Where(x => x.TenantId == tenantId);
723
+ // ...
724
+ }
725
+ ```
726
+
727
+ **Why it's wrong:** When a user hits an API via Swagger with a valid JWT but no tenant context (missing `X-Tenant-Slug` header), `TenantId` is null. The `!.Value` pattern produces an opaque `500 Internal Server Error` instead of a clear `401 Unauthorized` with an actionable message. The `?? throw new UnauthorizedAccessException(...)` pattern is caught by `GlobalExceptionHandlerMiddleware` and returned as a proper 401 response.
@@ -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)
@@ -62,13 +62,14 @@
62
62
  | Validate | MCP `validate_conventions` |
63
63
 
64
64
  **Rules:**
65
- - **ALL services MUST inject `ICurrentUser` and filter by `TenantId`** (see `smartstack-api.md` Service Pattern)
65
+ - **ALL services MUST inject `ICurrentUserService` + `ICurrentTenantService` and filter by `TenantId`** (see `smartstack-api.md` Service Pattern)
66
+ - **ALL services MUST use guard clause:** `var tenantId = _currentTenant.TenantId ?? throw new UnauthorizedAccessException("Tenant context is required");`
66
67
  - **ALL services MUST inject `ILogger<T>`** for production diagnostics
67
68
  - CQRS with MediatR
68
69
  - FluentValidation for all commands
69
70
  - DTOs separate from domain entities
70
71
  - Service interfaces in Application, implementations in Infrastructure
71
- - **FORBIDDEN:** `tenantId: Guid.Empty`, queries without TenantId filter, services without ICurrentUser
72
+ - **FORBIDDEN:** `tenantId: Guid.Empty`, `TenantId!.Value`, queries without TenantId filter, `ICurrentUser` (does not exist — use `ICurrentUserService` + `ICurrentTenantService`)
72
73
 
73
74
  ---
74
75
 
@@ -118,18 +119,28 @@
118
119
  3. **NavigationSectionSeedData.cs** — Section-level navigation (if sections defined)
119
120
  4. **NavigationResourceSeedData.cs** — Resource-level navigation (if resources defined)
120
121
  5. **PermissionsSeedData.cs** — MCP `generate_permissions` first, fallback template
121
- 6. **RolesSeedData.cs** — Context-based: Admin=CRUD, Manager=CRU, Contributor=CR, Viewer=R
122
+ 6. **RolesSeedData.cs** — Code-based role mapping: Admin=CRUD, Manager=CRU, Contributor=CR, Viewer=R. **NEVER use deterministic GUIDs for roles** — system roles are pre-seeded by SmartStack core, look up by Code at runtime.
122
123
  7. **{App}SeedDataProvider.cs** — Implements IClientSeedDataProvider
123
124
  - `SeedNavigationAsync()` — seeds Application → Module → Section → Resource + translations
124
- - `SeedPermissionsAsync()` + `SeedRolePermissionsAsync()`
125
+ - `SeedPermissionsAsync()` + `SeedRolePermissionsAsync()` (roles resolved by Code, NOT by GUID)
125
126
  - DI: `services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()`
126
127
 
127
128
  ### Deterministic GUID Pattern
128
129
 
130
+ > **CRITICAL — NavigationContext IDs are NEVER generated.**
131
+ > Contexts (`business`, `platform`, `personal`) are pre-seeded by SmartStack core with hardcoded GUIDs.
132
+ > Look up the context at runtime: `db.NavigationContexts.FirstOrDefaultAsync(c => c.Code == "business", ct)`
133
+ > **FORBIDDEN:** `GenerateDeterministicGuid("nav:business")` or any ContextId in SeedConstants.
134
+
129
135
  ```csharp
130
136
  // Use SHA256 for deterministic GUIDs (reproducible across environments)
137
+ // NOTE: Application/Module/Section/Resource IDs are deterministic.
138
+ // Context IDs are NOT — they are pre-seeded by SmartStack core.
131
139
  public static readonly Guid ModuleId = GenerateDeterministicGuid("nav-module-{app}-{module}");
132
140
 
141
+ // FORBIDDEN — Context IDs must NOT be generated:
142
+ // public static readonly Guid BusinessContextId = GenerateDeterministicGuid("nav:business"); // WRONG!
143
+
133
144
  private static Guid GenerateDeterministicGuid(string input)
134
145
  {
135
146
  var hash = System.Security.Cryptography.SHA256.HashData(
@@ -181,6 +192,7 @@ var sectionRoute = $"{moduleRoute}/{ToKebabCase(sectionCode)}";
181
192
 
182
193
  **FORBIDDEN:**
183
194
  - `Guid.NewGuid()` → use deterministic GUIDs (SHA256)
195
+ - `DeterministicGuid("nav:business")` for ContextId → context IDs are pre-seeded by SmartStack core, look up by code at runtime
184
196
  - Missing translations (must have fr, en, it, de)
185
197
  - Empty seed classes with no seeding logic
186
198
  - PascalCase in route URLs → always kebab-case
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: step-00-init
3
- description: Parse flags, detect SmartStack context, verify MCP availability
3
+ description: Parse flags, detect context, verify MCP, define hierarchy (5 levels), challenge the need
4
4
  model: sonnet
5
5
  next_step: steps/step-01-analyze.md
6
6
  ---
@@ -67,21 +67,7 @@ Scan the project to identify the SmartStack hierarchy:
67
67
  - `{prd_path}`: `.ralph/prd-{module_code}.json` if exists
68
68
  - `{feature_path}`: `docs/business/{app}/{module}/business-analyse/*/feature.json` if exists
69
69
 
70
- **If hierarchy cannot be inferred, ask the user:**
71
-
72
- ```yaml
73
- questions:
74
- - header: "Context"
75
- question: "What is the SmartStack context for this work?"
76
- options:
77
- - label: "Business (Recommended)"
78
- description: "Business application module"
79
- - label: "Platform"
80
- description: "Platform administration or support"
81
- - label: "Personal"
82
- description: "Personal user space"
83
- multiSelect: false
84
- ```
70
+ If hierarchy cannot be inferred load context detection question from `references/challenge-questions.md`.
85
71
 
86
72
  ---
87
73
 
@@ -96,70 +82,112 @@ IF failure → warn user, continue in degraded mode (manual tools only)
96
82
 
97
83
  ---
98
84
 
99
- ## 4. Determine Needs
85
+ ## 4. Define Navigation Hierarchy (5 Levels)
86
+
87
+ > **BLOCKING RULE:** SmartStack uses a 5-level navigation hierarchy:
88
+ > **Context → Application → Module → Section → Resource**
89
+ >
90
+ > **Every module MUST have at least one section.** A module without sections produces an incomplete navigation tree, broken seed data, and missing frontend routes.
91
+
92
+ ### 4a. Validate Application Level
93
+
94
+ Load and execute application question from `references/challenge-questions.md` section 4a.
95
+
96
+ ### 4b. Validate Module Level
97
+
98
+ Load and execute module question from `references/challenge-questions.md` section 4b.
99
+
100
+ ### 4c. Define Sections (MANDATORY — at least one)
101
+
102
+ Load and execute section questions from `references/challenge-questions.md` section 4c (includes validation and storage format).
100
103
 
104
+ ### 4d. Resources (Optional)
105
+
106
+ If the task or feature.json mentions specific resources (export, import, bulk actions):
107
+ ```
108
+ For each section, check if resources are needed.
109
+ Resources are OPTIONAL — only define if explicitly mentioned or inferred.
101
110
  ```
102
- needs_seed_data = true IF:
103
- - New module or new section with navigation entries
104
- - Task mentions "navigation", "permissions", "roles", "seed"
105
111
 
106
- needs_migration = true IF:
107
- - New entities or entity modifications detected
108
- - Task mentions "entity", "field", "table", "column", "migration"
112
+ ### 4e. Hierarchy Summary
113
+
114
+ Display before proceeding:
115
+
116
+ ```
117
+ {context_code} → {app_name} ({app_code}) → {module_code} → [{sections[].code}]
118
+ Routes: /{context_code}/{app-kebab}/{module-kebab}/{section-kebab}
109
119
  ```
110
120
 
111
121
  ---
112
122
 
113
- ## 5. Resume Mode (if -r)
123
+ ## 5. Challenge the Need
124
+
125
+ > **Objective:** Even without a formal Business Analysis (`/business-analyse`), the need MUST be challenged to avoid obvious gaps. This is a rapid scope validation — 3-4 targeted questions to ensure the developer has thought through the fundamentals.
126
+
127
+ ### 5a. Entity Scope & Relationships
128
+
129
+ Load and execute entity questions from `references/challenge-questions.md` section 5a.
130
+
131
+ ### 5b. Dependencies & Key Properties
132
+
133
+ Load and execute dependency questions from `references/challenge-questions.md` section 5b.
114
134
 
135
+ ### 5c. Store Responses
136
+
137
+ ```yaml
138
+ {entities}: string[] # Main entities to manage
139
+ {module_complexity}: string # "simple-crud" | "crud-rules" | "crud-workflow" | "complex"
140
+ {has_dependencies}: string # "none" | "references" | "unknown"
141
+ {key_properties}: string[] # Key business properties mentioned by user
115
142
  ```
116
- IF resume_mode:
117
- 1. Read .claude/output/apex/ find latest task folder
118
- 2. Read 00-context.md restore state variables
119
- 3. Find last completed step file
120
- 4. Route to next step directly
121
- → SKIP to identified next step
143
+
144
+ These values are propagated to:
145
+ - **step-01 (Analyze):** Guide code exploration focus
146
+ - **step-02 (Plan):** Inform layer mapping (e.g., workflow needs /workflow skill)
147
+ - **step-03 (Execute):** Entity scaffolding with correct properties and relationships
148
+
149
+ ---
150
+
151
+ ## 6. Determine Needs
152
+
153
+ ```
154
+ needs_seed_data = {sections}.length > 0 OR task mentions navigation/permissions/roles/seed
155
+ needs_migration = {entities}.length > 0 OR task mentions entity/field/table/column/migration
156
+ needs_workflow = {module_complexity} == "crud-workflow" OR task mentions workflow/email/approval
157
+ needs_notification = {module_complexity} in ["crud-workflow","complex"] OR task mentions notification/alert
122
158
  ```
123
159
 
124
160
  ---
125
161
 
126
- ## 6. Save Mode Setup (if -s)
162
+ ## 7. Resume Mode (if -r)
127
163
 
128
164
  ```
129
- IF save_mode:
130
- 1. Generate task_id: NN-kebab-case from task_description
131
- 2. Create .claude/output/apex/{task_id}/
132
- 3. Write 00-context.md with:
133
- - Timestamp
134
- - All state variables (generic + SmartStack)
135
- - Flags
136
- - Detected hierarchy
165
+ IF resume_mode:
166
+ Read .claude/output/apex/ find latest task → restore state from 00-context.md → SKIP to next step
137
167
  ```
138
168
 
139
169
  ---
140
170
 
141
- ## 7. Display Context Summary
171
+ ## 8. Save Mode Setup (if -s)
142
172
 
143
173
  ```
144
- ═══════════════════════════════════════════════════════════════
145
- APEX INITIALIZATION COMPLETE
146
- SmartStack CLI v{{SMARTSTACK_VERSION}}
147
- ═══════════════════════════════════════════════════════════════
174
+ IF save_mode:
175
+ Generate task_id → create .claude/output/apex/{task_id}/ → write 00-context.md
176
+ (timestamp, state variables, flags, hierarchy, challenge responses)
177
+ ```
148
178
 
149
- | Field | Value |
150
- |--------------------|----------------------------------------------|
151
- | Task | {task_description} |
152
- | Context | {context_code} / {app_name} / {module_code} |
153
- | Sections | {sections} |
154
- | PRD | {prd_path || "none"} |
155
- | Feature | {feature_path || "none"} |
156
- | Flags | {active_flags} |
157
- | MCP | {available|degraded} |
158
- | Needs migration | {yes|no} |
159
- | Needs seed data | {yes|no} |
179
+ ---
180
+
181
+ ## 9. Display Context Summary
160
182
 
183
+ ```
184
+ APEX INIT COMPLETE — v{{SMARTSTACK_VERSION}}
185
+ Task: {task_description}
186
+ Context: {context_code} → {app_name} → {module_code} → {sections[].code}
187
+ Entities: {entities} | Complexity: {module_complexity} | Deps: {has_dependencies}
188
+ PRD: {prd_path||none} | Feature: {feature_path||none} | Flags: {active_flags}
189
+ MCP: {available|degraded} | Needs: migration/seed/workflow/notification = {yes|no}
161
190
  NEXT STEP: step-01-analyze
162
- ═══════════════════════════════════════════════════════════════
163
191
  ```
164
192
 
165
193
  ---