@atlashub/smartstack-cli 3.28.0 → 3.29.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 (42) hide show
  1. package/dist/index.js +6 -7
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -3
  4. package/templates/project/api.ts.template +4 -2
  5. package/templates/project/appsettings.json.template +1 -1
  6. package/templates/skills/apex/_shared.md +13 -0
  7. package/templates/skills/apex/references/post-checks.md +228 -6
  8. package/templates/skills/apex/references/smartstack-api.md +67 -17
  9. package/templates/skills/apex/references/smartstack-frontend.md +41 -1
  10. package/templates/skills/apex/references/smartstack-layers.md +40 -10
  11. package/templates/skills/apex/steps/step-02-plan.md +16 -11
  12. package/templates/skills/apex/steps/step-03-execute.md +6 -0
  13. package/templates/skills/apex/steps/step-04-examine.md +4 -2
  14. package/templates/skills/application/references/frontend-verification.md +26 -1
  15. package/templates/skills/application/steps/step-03-roles.md +1 -1
  16. package/templates/skills/application/steps/step-05-frontend.md +24 -8
  17. package/templates/skills/application/templates-frontend.md +41 -22
  18. package/templates/skills/application/templates-seed.md +53 -16
  19. package/templates/skills/business-analyse/SKILL.md +4 -2
  20. package/templates/skills/business-analyse/_shared.md +17 -4
  21. package/templates/skills/business-analyse/react/schema.md +1 -1
  22. package/templates/skills/business-analyse/references/agent-module-prompt.md +11 -9
  23. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +4 -3
  24. package/templates/skills/business-analyse/references/deploy-modes.md +1 -1
  25. package/templates/skills/business-analyse/references/handoff-file-templates.md +4 -4
  26. package/templates/skills/business-analyse/references/robustness-checks.md +12 -9
  27. package/templates/skills/business-analyse/references/spec-auto-inference.md +3 -3
  28. package/templates/skills/business-analyse/references/ui-resource-cards.md +3 -3
  29. package/templates/skills/business-analyse/references/validation-checklist.md +21 -3
  30. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +33 -5
  31. package/templates/skills/business-analyse/steps/step-03b-ui.md +2 -2
  32. package/templates/skills/business-analyse/steps/step-03c-compile.md +17 -9
  33. package/templates/skills/business-analyse/steps/step-03d-validate.md +1 -1
  34. package/templates/skills/business-analyse/steps/step-04b-analyze.md +5 -3
  35. package/templates/skills/business-analyse/steps/step-05a-handoff.md +23 -15
  36. package/templates/skills/business-analyse/templates/tpl-handoff.md +10 -8
  37. package/templates/skills/business-analyse/templates/tpl-progress.md +7 -6
  38. package/templates/skills/ralph-loop/references/category-rules.md +50 -6
  39. package/templates/skills/ralph-loop/references/compact-loop.md +16 -1
  40. package/templates/skills/ralph-loop/references/core-seed-data.md +158 -38
  41. package/templates/skills/ralph-loop/references/task-transform-legacy.md +3 -3
  42. package/templates/skills/ralph-loop/steps/step-02-execute.md +109 -1
@@ -79,25 +79,50 @@ Execution sequence:
79
79
  > "PermissionsSeedData", "RolesSeedData", or "IClientSeedDataProvider":
80
80
  > **THEN read `references/core-seed-data.md`** — this is MANDATORY, DO NOT improvise.
81
81
 
82
- **Rules:**
83
- - NavigationModuleSeedData.cs: deterministic GUIDs (SHA256), 4 languages (fr, en, it, de)
84
- - PermissionsSeedData.cs: MCP `generate_permissions` first, fallback to template
85
- - RolesSeedData.cs: context-based role mapping (Admin=CRUD, Manager=CRU, Contributor=CR, Viewer=R)
86
- - SeedConstants.cs: shared deterministic GUIDs
87
- - IClientSeedDataProvider: SeedNavigationAsync + SeedPermissionsAsync + SeedRolePermissionsAsync
82
+ **Seed Data Chain (9 files minimum):**
83
+
84
+ Application-level (created ONCE, shared across modules):
85
+ - **NavigationApplicationSeedData.cs**: Application navigation entry (MUST be first). Provides ApplicationId.
86
+ - **ApplicationRolesSeedData.cs**: 4 application-scoped roles (admin, manager, contributor, viewer). Provides role entries for SeedRolesAsync().
87
+
88
+ Per-module:
89
+ - **NavigationModuleSeedData.cs**: deterministic GUIDs (SHA256), 4 languages (fr, en, it, de)
90
+ - **Permissions.cs**: Static permission constants (`Permissions.{Module}.Read`). Referenced by `[RequirePermission]`.
91
+ - **PermissionsSeedData.cs**: MCP `generate_permissions` first, fallback to template
92
+ - **RolesSeedData.cs**: code-based role-permission mapping (Admin=wildcard, Manager=CRU, Contributor=CR, Viewer=R)
93
+
94
+ Per-module (MANDATORY when `seedDataCore.navigationSections` exists in feature.json):
95
+ - **NavigationSectionSeedData**: section entries, section translations (4 languages), section-level permissions, and section-level role mappings — all within `NavigationModuleSeedData.cs`, `PermissionsSeedData.cs`, and `RolesSeedData.cs`
96
+ - Section-level permissions: wildcard + CRUD per section (same pattern as module-level)
97
+ - Section-level role mappings: Admin=wildcard, Manager=CRU, Contributor=CR, Viewer=R per section
98
+
99
+ Infrastructure:
100
+ - **SeedConstants.cs**: shared deterministic GUIDs (ApplicationId, ModuleIds)
101
+ - **{App}SeedDataProvider.cs**: implements IClientSeedDataProvider with 4 methods
88
102
  - DI: `services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()`
89
103
 
104
+ **Rules:**
105
+ - IClientSeedDataProvider: SeedNavigationAsync + SeedRolesAsync + SeedPermissionsAsync + SeedRolePermissionsAsync
106
+ - Admin=wildcard(*), Manager=CRU (read+create+update), Contributor=CR (read+create), Viewer=R (read only)
107
+
90
108
  **Business seed data (DevDataSeeder):**
91
109
  - ALL seeded business entities MUST include `TenantId = {tenantGuid}`
92
110
  - Reference entities (types, categories, statuses) MUST set TenantId
93
111
  - Use deterministic TenantId from SeedConstants (NEVER hardcoded inline)
94
112
  - DevDataSeeder MUST implement `IDevDataSeeder` with idempotent `SeedAsync()` method
95
113
 
114
+ **Section route conventions (BLOCKING):**
115
+ - `list` section route = module route (e.g., `/business/human-resources/employees`) — NO `/list` suffix
116
+ - `detail` section route = module route + `/:id` (e.g., `/business/human-resources/employees/:id`) — NOT `/detail/:id`
117
+ - Other sections (dashboard, approve, import) = module route + `/{section-kebab}` (normal)
118
+ - FORBIDDEN: `/{module}/list`, `/{module}/detail/:id` — these are CRUD view modes, not sub-areas
119
+
96
120
  **FORBIDDEN:**
97
121
  - `Guid.NewGuid()` → use deterministic GUIDs
98
122
  - Empty seed data classes with only GUIDs and no seeding methods
99
123
  - Missing translations (must have all 4 languages)
100
124
  - Seeding business entities WITHOUT `TenantId`
125
+ - Navigation section routes ending in `/list` or `/detail/:id`
101
126
 
102
127
  ### POST-CHECK: Navigation translations diacritical marks
103
128
 
@@ -302,6 +327,13 @@ fi
302
327
  - Reference/lookup entities (types, categories, statuses) MUST also have controllers — they are needed for dropdowns and configuration
303
328
  - Count: `controllers created >= entities in module`. If fewer → FAIL
304
329
 
330
+ **Section-level controllers (CONDITIONAL: when `navSections[]` defined in feature.json):**
331
+ - File path: `Api/Controllers/{ContextShort}/{App}/{Section}Controller.cs`
332
+ - NavRoute attribute: `[NavRoute("{context}.{app}.{module}.{section}")]`
333
+ - Permission attribute: `[RequirePermission(Permissions.{Module}.{Section}.{Action})]`
334
+ - Route prefix: `api/{context}/{app}/{module}/{section}`
335
+ - Each section gets its own controller with CRUD endpoints scoped to the section
336
+
305
337
  **FORBIDDEN:**
306
338
  - `[Authorize]` without specific permission → use `[RequirePermission]`
307
339
  - Returning domain entities directly
@@ -374,6 +406,18 @@ fi
374
406
  - Navigation seed data Route values MUST also use kebab-case
375
407
  - The POST-CHECK in step-02 will BLOCK if frontend routes don't match backend kebab-case convention
376
408
 
409
+ **Section-level pages (CONDITIONAL: when `navSections[]` defined in feature.json):**
410
+ - Page file: `src/pages/{ContextPascal}/{AppPascal}/{Module}/{Section}Page.tsx`
411
+ - Route: nested as child of module route in App.tsx (e.g., `/business/human-resources/projects/timesheets`)
412
+ - Add to `contextRoutes.{context}[]` (Pattern A) or as nested `<Route>` (Pattern B)
413
+ - Each section page has its own route and permission check
414
+
415
+ **React Router mapping for sections:**
416
+ - `list` section → already the module's `index: true` route (NO separate `path: 'list'`)
417
+ - `detail` section → already the module's `path: ':id'` route (NO separate `path: 'detail'`)
418
+ - Other sections → `path: '{section-kebab}'` as child of module route
419
+ - FORBIDDEN frontend paths: `path: 'list'`, `path: 'detail'` — these are handled by the module's index and :id routes
420
+
377
421
  **CSS:** Variables ONLY → `bg-[var(--bg-card)]`, `text-[var(--text-primary)]`
378
422
 
379
423
  **Form error handling (MANDATORY):**
@@ -309,7 +309,22 @@ Batch: {batch.length} [{firstCategory}] → {batch.map(t => `[${t.id}] ${t.descr
309
309
  ```
310
310
  If FAIL → migration is broken → fix → rebuild → DO NOT commit
311
311
 
312
- 5bis. **Core Seed Data Integrity (BLOCKING — if seed data tasks in batch):**
312
+ 5bis. **Navigation section route CRUD suffix check (BLOCKING — if seed data tasks in batch):**
313
+ ```bash
314
+ # Detect /list or /detail/:id in navigation section routes
315
+ SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null)
316
+ if [ -n "$SEED_NAV_FILES" ]; then
317
+ CRUD_ROUTES=$(grep -rn 'Route.*=.*\/list\b\|Route.*=.*\/detail' $SEED_NAV_FILES 2>/dev/null | grep -v '//.*Route')
318
+ if [ -n "$CRUD_ROUTES" ]; then
319
+ echo "BLOCKING: Navigation section routes contain /list or /detail/:id suffixes"
320
+ echo "Convention: list → module route (no suffix), detail → module route + /:id"
321
+ echo "$CRUD_ROUTES"
322
+ # FIX REQUIRED before commit
323
+ fi
324
+ fi
325
+ ```
326
+
327
+ 5ter. **Core Seed Data Integrity (BLOCKING — if seed data tasks in batch):**
313
328
  ```bash
314
329
  # Verify NavigationApplicationSeedData.cs exists
315
330
  APP_SEED=$(find . -path "*/Seeding/Data/NavigationApplicationSeedData.cs" 2>/dev/null | head -1)
@@ -415,6 +415,13 @@ public static readonly Guid {ResourcePascal}ResourceId =
415
415
 
416
416
  ### Section Methods (add to {ModulePascal}NavigationSeedData.cs)
417
417
 
418
+ > **ROUTE SPECIAL CASES (list and detail):**
419
+ > The `list` and `detail` sections are view modes of the module, NOT functional sub-areas.
420
+ > - `list` section route = module route (e.g., `/business/human-resources/employees`) — NO `/list` suffix
421
+ > - `detail` section route = module route + `/:id` (e.g., `/business/human-resources/employees/:id`) — NOT `/detail/:id`
422
+ > - FORBIDDEN: `/{module}/list`, `/{module}/detail/:id`
423
+ > - Other sections (dashboard, approve, import) = module route + `/{section-kebab}` (normal)
424
+
418
425
  ```csharp
419
426
  // --- Add AFTER GetTranslationEntries() in {ModulePascal}NavigationSeedData.cs ---
420
427
 
@@ -439,7 +446,12 @@ public static IEnumerable<NavigationSectionSeedEntry> GetSectionEntries(Guid mod
439
446
  Description = "{section1_desc_en}",
440
447
  Icon = "{section1_icon}",
441
448
  IconType = IconType.Lucide,
442
- Route = ToKebabCase($"/{contextCode}/{appCode}/{moduleCode}/{section1Code}"),
449
+ // ROUTE CONVENTION:
450
+ // - "list" section → same as module route (no extra segment)
451
+ // - "detail" section → module route + "/:id"
452
+ // - Other sections → module route + "/{section-kebab}"
453
+ // FORBIDDEN: "/employees/list", "/employees/detail/:id"
454
+ Route = "{section_route}", // From seedDataCore.navigationSections[].route
443
455
  DisplayOrder = {section1_sort},
444
456
  IsActive = true
445
457
  }
@@ -521,6 +533,17 @@ public static IEnumerable<NavigationResourceSeedEntry> GetResourceEntries(Guid s
521
533
  {
522
534
  entries.AddRange(new[]
523
535
  {
536
+ // RESOURCE ROUTE CONVENTION:
537
+ // Resources inherit their parent section's resolved route as base:
538
+ // - Under "list" section → base = module route (no /list)
539
+ // - Under "detail" section → base = module route (no /detail, resource routes don't include /:id)
540
+ // - Under other sections → base = module route + /{section-kebab}
541
+ // Then append: /{resource-kebab}
542
+ //
543
+ // Example: resource "export" under section "dashboard":
544
+ // Route = /business/human-resources/employees/dashboard/export
545
+ // Example: resource "employees-grid" under section "list":
546
+ // Route = /business/human-resources/employees/employees-grid (NOT /employees/list/employees-grid)
524
547
  new NavigationResourceSeedEntry
525
548
  {
526
549
  Id = {Resource1Pascal}ResourceId,
@@ -528,7 +551,9 @@ public static IEnumerable<NavigationResourceSeedEntry> GetResourceEntries(Guid s
528
551
  Code = "{resource1Code}",
529
552
  Label = "{resource1_label_en}",
530
553
  EntityType = "{resource1_entity}",
531
- Route = ToKebabCase($"/{contextCode}/{appCode}/{moduleCode}/{section1Code}/{resource1Code}"),
554
+ // Use parent section's resolved route + /{resource-kebab}
555
+ // For "list"/"detail" sections, the section route = module route (no /list or /detail segment)
556
+ Route = "{resource_route}", // From seedDataCore: parent section route + /{resource-kebab}
532
557
  DisplayOrder = 1
533
558
  }
534
559
  // Repeat for each resource in this section...
@@ -579,9 +604,10 @@ public class NavigationResourceSeedEntry
579
604
  | `{section_label_xx}` | `specification.navigation.entries[]` where `level == "section"` → `labels.xx` |
580
605
  | `{section_icon}` | `seedDataCore.navigationSections[].icon` |
581
606
  | `{section_sort}` | `seedDataCore.navigationSections[].sort` |
582
- | `{section_route}` | `seedDataCore.navigationSections[].route` |
607
+ | `{section_route}` | `seedDataCore.navigationSections[].route` — **SPECIAL CASES:** `list` → module route (no `/list`), `detail` → module route + `/:id` (no `/detail/:id`), others → module route + `/{section-kebab}` |
583
608
  | `{resourceCode}` | `seedDataCore.navigationResources[].code` |
584
609
  | `{resource_entity}` | `seedDataCore.navigationResources[].entity` |
610
+ | `{resource_route}` | Computed from parent section route + `/{resource-kebab}`. **SPECIAL CASES:** if parent section is `list` → module route + `/{resource-kebab}` (no `/list/`), if parent is `detail` → module route + `/{resource-kebab}` (no `/detail/`). |
585
611
  | `{parentSectionCode}` | `seedDataCore.navigationResources[].parentCode` |
586
612
 
587
613
  ---
@@ -718,6 +744,50 @@ public class PermissionSeedEntry
718
744
  }
719
745
  ```
720
746
 
747
+ ### Step C2: Section-Level Permissions (CONDITIONAL: only if `navSections[]` defined)
748
+
749
+ > When `seedDataCore.navigationSections` exists and is non-empty in feature.json,
750
+ > add section-level permission GUIDs and entries to `PermissionsSeedData.cs`.
751
+
752
+ ```csharp
753
+ // --- Add to {ModulePascal}PermissionsSeedData class AFTER module-level permissions ---
754
+
755
+ // Section-level permissions (for each section in navSections[])
756
+ public static readonly Guid {SectionPascal}WildcardPermId = GenerateGuid("{navRoute}.{sectionCode}.*");
757
+ public static readonly Guid {SectionPascal}ReadPermId = GenerateGuid("{navRoute}.{sectionCode}.read");
758
+ public static readonly Guid {SectionPascal}CreatePermId = GenerateGuid("{navRoute}.{sectionCode}.create");
759
+ public static readonly Guid {SectionPascal}UpdatePermId = GenerateGuid("{navRoute}.{sectionCode}.update");
760
+ public static readonly Guid {SectionPascal}DeletePermId = GenerateGuid("{navRoute}.{sectionCode}.delete");
761
+ // Repeat for each section...
762
+
763
+ // Add to GetPermissionEntries() — AFTER module-level entries:
764
+ // Section: {sectionCode}
765
+ new PermissionSeedEntry { Id = {SectionPascal}WildcardPermId, Path = "{navRoute}.{sectionCode}.*", Level = PermissionLevel.Section, Action = PermissionAction.Access, IsWildcard = true, ModuleId = moduleId, Description = "Full {sectionLabel} access" },
766
+ new PermissionSeedEntry { Id = {SectionPascal}ReadPermId, Path = "{navRoute}.{sectionCode}.read", Level = PermissionLevel.Section, Action = PermissionAction.Read, IsWildcard = false, ModuleId = moduleId, Description = "View {sectionLabel}" },
767
+ new PermissionSeedEntry { Id = {SectionPascal}CreatePermId, Path = "{navRoute}.{sectionCode}.create", Level = PermissionLevel.Section, Action = PermissionAction.Create, IsWildcard = false, ModuleId = moduleId, Description = "Create {sectionLabel}" },
768
+ new PermissionSeedEntry { Id = {SectionPascal}UpdatePermId, Path = "{navRoute}.{sectionCode}.update", Level = PermissionLevel.Section, Action = PermissionAction.Update, IsWildcard = false, ModuleId = moduleId, Description = "Update {sectionLabel}" },
769
+ new PermissionSeedEntry { Id = {SectionPascal}DeletePermId, Path = "{navRoute}.{sectionCode}.delete", Level = PermissionLevel.Section, Action = PermissionAction.Delete, IsWildcard = false, ModuleId = moduleId, Description = "Delete {sectionLabel}" },
770
+ // Repeat for each section...
771
+ ```
772
+
773
+ Also add section-level constants to `Permissions.cs` (Application layer):
774
+
775
+ ```csharp
776
+ public static class {ModulePascal}
777
+ {
778
+ // ... existing module-level permissions ...
779
+
780
+ // Section-level (for each section in navSections[])
781
+ public static class {SectionPascal}
782
+ {
783
+ public const string View = "{navRoute}.{sectionCode}.read";
784
+ public const string Create = "{navRoute}.{sectionCode}.create";
785
+ public const string Update = "{navRoute}.{sectionCode}.update";
786
+ public const string Delete = "{navRoute}.{sectionCode}.delete";
787
+ }
788
+ }
789
+ ```
790
+
721
791
  ### Step D: MCP Fallback
722
792
 
723
793
  If MCP `generate_permissions` fails, use the template above directly with values derived from the PRD `coreSeedData.permissions[]`.
@@ -897,7 +967,7 @@ public static class {ModulePascal}RolesSeedData
897
967
  // Admin: wildcard access
898
968
  yield return new RolePermissionSeedEntry { RoleCode = "admin", PermissionPath = "{navRoute}.*" };
899
969
 
900
- // Manager: CRUD
970
+ // Manager: CRU (read + create + update — no delete)
901
971
  yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.read" };
902
972
  yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.create" };
903
973
  yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.update" };
@@ -908,6 +978,23 @@ public static class {ModulePascal}RolesSeedData
908
978
 
909
979
  // Viewer: R
910
980
  yield return new RolePermissionSeedEntry { RoleCode = "viewer", PermissionPath = "{navRoute}.read" };
981
+
982
+ // --- Section-level role mappings (CONDITIONAL: for each section in navSections[]) ---
983
+ // Admin: wildcard per section
984
+ yield return new RolePermissionSeedEntry { RoleCode = "admin", PermissionPath = "{navRoute}.{sectionCode}.*" };
985
+
986
+ // Manager: CRU per section (read + create + update — no delete)
987
+ yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.{sectionCode}.read" };
988
+ yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.{sectionCode}.create" };
989
+ yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.{sectionCode}.update" };
990
+
991
+ // Contributor: CR per section
992
+ yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.{sectionCode}.read" };
993
+ yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.{sectionCode}.create" };
994
+
995
+ // Viewer: R per section
996
+ yield return new RolePermissionSeedEntry { RoleCode = "viewer", PermissionPath = "{navRoute}.{sectionCode}.read" };
997
+ // Repeat block for each section in navSections[]...
911
998
  }
912
999
  }
913
1000
 
@@ -960,43 +1047,63 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
960
1047
  public async Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct)
961
1048
  {
962
1049
  // --- Application (from NavigationApplicationSeedData) ---
1050
+ // NOTE: Idempotence is at MODULE level (not application level).
1051
+ // If the application already exists, we load it and continue to seed any missing modules.
1052
+ // This allows adding Module 2+ to an existing application without re-running the full seed.
963
1053
  var appEntry = NavigationApplicationSeedData.GetApplicationEntry(Guid.Empty); // contextId resolved below
964
- var exists = await context.NavigationApplications
965
- .AnyAsync(a => a.Code == appEntry.Code, ct);
966
- if (exists) return;
1054
+ var existingApp = await context.NavigationApplications
1055
+ .FirstOrDefaultAsync(a => a.Code == appEntry.Code, ct);
967
1056
 
968
- var parentContext = await context.NavigationContexts
969
- .FirstAsync(c => c.Code == "{contextCode}", ct);
970
-
971
- // Re-get entry with resolved contextId
972
- appEntry = NavigationApplicationSeedData.GetApplicationEntry(parentContext.Id);
973
- var app = NavigationApplication.Create(
974
- appEntry.ContextId, appEntry.Code, appEntry.Label,
975
- appEntry.Description, appEntry.Icon, appEntry.IconType,
976
- appEntry.Route, appEntry.DisplayOrder);
977
- context.NavigationApplications.Add(app);
978
- await ((DbContext)context).SaveChangesAsync(ct);
979
-
980
- // --- Application translations (4 languages, from NavigationApplicationSeedData) ---
981
- foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
1057
+ NavigationApplication app;
1058
+ if (existingApp != null)
982
1059
  {
983
- context.NavigationTranslations.Add(
984
- NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
1060
+ app = existingApp; // Application already seeded — reuse it for module seeding below
1061
+ }
1062
+ else
1063
+ {
1064
+ var parentContext = await context.NavigationContexts
1065
+ .FirstAsync(c => c.Code == "{contextCode}", ct);
1066
+
1067
+ // Re-get entry with resolved contextId
1068
+ appEntry = NavigationApplicationSeedData.GetApplicationEntry(parentContext.Id);
1069
+ app = NavigationApplication.Create(
1070
+ appEntry.ContextId, appEntry.Code, appEntry.Label,
1071
+ appEntry.Description, appEntry.Icon, appEntry.IconType,
1072
+ appEntry.Route, appEntry.DisplayOrder);
1073
+ context.NavigationApplications.Add(app);
1074
+ await ((DbContext)context).SaveChangesAsync(ct);
1075
+
1076
+ // --- Application translations (4 languages, from NavigationApplicationSeedData) ---
1077
+ foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
1078
+ {
1079
+ context.NavigationTranslations.Add(
1080
+ NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
1081
+ }
1082
+ await ((DbContext)context).SaveChangesAsync(ct);
985
1083
  }
986
- await ((DbContext)context).SaveChangesAsync(ct);
987
1084
 
988
- // --- Modules ---
1085
+ // --- Modules (idempotent per-module — allows adding Module 2+ later) ---
989
1086
  // Module: {Module1}
990
- var mod1Entry = {Module1Pascal}NavigationSeedData.GetModuleEntry(app.Id);
991
- var mod1 = NavigationModule.Create(
992
- mod1Entry.ApplicationId, mod1Entry.Code, mod1Entry.Label,
993
- mod1Entry.Description, mod1Entry.Icon, mod1Entry.IconType,
994
- mod1Entry.Route, mod1Entry.DisplayOrder);
995
- context.NavigationModules.Add(mod1);
1087
+ var mod1Exists = await context.NavigationModules
1088
+ .AnyAsync(m => m.Code == "{module1Code}" && m.ApplicationId == app.Id, ct);
1089
+ if (!mod1Exists)
1090
+ {
1091
+ var mod1Entry = {Module1Pascal}NavigationSeedData.GetModuleEntry(app.Id);
1092
+ var mod1 = NavigationModule.Create(
1093
+ mod1Entry.ApplicationId, mod1Entry.Code, mod1Entry.Label,
1094
+ mod1Entry.Description, mod1Entry.Icon, mod1Entry.IconType,
1095
+ mod1Entry.Route, mod1Entry.DisplayOrder);
1096
+ context.NavigationModules.Add(mod1);
1097
+ }
996
1098
 
997
- // Repeat for each module...
1099
+ // Repeat for each module (each with its own idempotence check)...
998
1100
  await ((DbContext)context).SaveChangesAsync(ct);
999
1101
 
1102
+ // Resolve module entities for section/resource seeding (works for both new AND existing modules)
1103
+ var mod1Entity = await context.NavigationModules
1104
+ .FirstAsync(m => m.Code == "{module1Code}" && m.ApplicationId == app.Id, ct);
1105
+ // Repeat for each module...
1106
+
1000
1107
  // --- Module translations ---
1001
1108
  foreach (var t in {Module1Pascal}NavigationSeedData.GetTranslationEntries())
1002
1109
  {
@@ -1008,7 +1115,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1008
1115
 
1009
1116
  // --- Sections (if seedDataCore.navigationSections exists) ---
1010
1117
  // Module 1 sections
1011
- foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1.Id))
1118
+ foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
1012
1119
  {
1013
1120
  var sec = NavigationSection.Create(
1014
1121
  secEntry.ModuleId, secEntry.Code, secEntry.Label,
@@ -1030,7 +1137,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1030
1137
 
1031
1138
  // --- Resources (if seedDataCore.navigationResources exists) ---
1032
1139
  // Module 1 resources (resolved per section)
1033
- foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1.Id))
1140
+ foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
1034
1141
  {
1035
1142
  foreach (var resEntry in {Module1Pascal}NavigationSeedData.GetResourceEntries(secEntry.Id))
1036
1143
  {
@@ -1102,8 +1209,9 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1102
1209
  if (exists) return;
1103
1210
 
1104
1211
  // CRITICAL: Resolve roles by Code from DB — NEVER use deterministic GUIDs.
1105
- // System roles (admin, manager, contributor, viewer) are pre-seeded by SmartStack core
1106
- // with their own IDs that do NOT match DeterministicGuid("role:admin").
1212
+ // Application-scoped roles (admin, manager, contributor, viewer) are created by
1213
+ // SeedRolesAsync() above. System roles use their own IDs that do NOT match
1214
+ // DeterministicGuid("role:admin").
1107
1215
  var roles = await context.Roles
1108
1216
  .Where(r => r.ApplicationId != null || r.IsSystem)
1109
1217
  .ToListAsync(ct);
@@ -1122,10 +1230,22 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1122
1230
  {
1123
1231
  var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);
1124
1232
  var perm = permissions.FirstOrDefault(p => p.Path == mapping.PermissionPath);
1125
- if (role != null && perm != null)
1233
+
1234
+ if (role == null)
1126
1235
  {
1127
- context.RolePermissions.Add(RolePermission.Create(role.Id, perm.Id, "system"));
1236
+ // CRITICAL: Role not found — SeedRolesAsync() may not have run.
1237
+ // This causes silent permission failure → 401 on protected pages.
1238
+ Console.WriteLine($"[SEED WARNING] Role '{mapping.RoleCode}' not found. Role-permission mapping skipped for '{mapping.PermissionPath}'.");
1239
+ continue;
1128
1240
  }
1241
+
1242
+ if (perm == null)
1243
+ {
1244
+ Console.WriteLine($"[SEED WARNING] Permission '{mapping.PermissionPath}' not found. Role-permission mapping skipped for role '{mapping.RoleCode}'.");
1245
+ continue;
1246
+ }
1247
+
1248
+ context.RolePermissions.Add(RolePermission.Create(role.Id, perm.Id, "system"));
1129
1249
  }
1130
1250
 
1131
1251
  await ((DbContext)context).SaveChangesAsync(ct);
@@ -177,9 +177,9 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
177
177
  const permissions = coreSeedData.permissions || [];
178
178
  const rolePerms = coreSeedData.rolePermissions || [];
179
179
  tasks.push({
180
- id: taskId, description: `[infrastructure] Create core seed data for ${moduleCode}: NavigationModuleSeedData, PermissionsSeedData, RolesSeedData`,
180
+ id: taskId, description: `[infrastructure] Create core seed data for ${moduleCode}: NavigationApplicationSeedData, ApplicationRolesSeedData, NavigationModuleSeedData, PermissionsSeedData, RolesSeedData`,
181
181
  status: "pending", category: "infrastructure", dependencies: infraDepId ? [infraDepId] : [],
182
- acceptance_criteria: `NavigationModuleSeedData (${navModules.length} nav); PermissionsSeedData (${permissions.length} perms); RolesSeedData (${rolePerms.length} roles); SeedConstants; dotnet build passes`,
182
+ acceptance_criteria: `NavigationApplicationSeedData (app-level, once); ApplicationRolesSeedData (4 roles); NavigationModuleSeedData (${navModules.length} nav); PermissionsSeedData (${permissions.length} perms); RolesSeedData (${rolePerms.length} roles); SeedConstants; dotnet build passes`,
183
183
  started_at: null, completed_at: null, iteration: null, commit_hash: null,
184
184
  files_changed: { created: [], modified: [] }, validation: null, error: null, module: moduleCode,
185
185
  _seedDataMeta: {
@@ -199,7 +199,7 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
199
199
  id: taskId, description: `[infrastructure] Create IClientSeedDataProvider for core seed data injection`,
200
200
  status: "pending", category: "infrastructure",
201
201
  dependencies: [lastIdByCategory["infrastructure"] || lastIdByCategory["domain"]].filter(Boolean),
202
- acceptance_criteria: "SeedNavigationAsync + SeedPermissionsAsync + SeedRolePermissionsAsync; DI registered; idempotent",
202
+ acceptance_criteria: "SeedNavigationAsync + SeedRolesAsync + SeedPermissionsAsync + SeedRolePermissionsAsync; DI registered; idempotent",
203
203
  started_at: null, completed_at: null, iteration: null, commit_hash: null,
204
204
  files_changed: { created: [], modified: [] }, validation: null, error: null, module: moduleCode,
205
205
  _providerMeta: {
@@ -178,6 +178,102 @@ fi
178
178
  - If `auth_Permissions` is empty → all authorization checks reject → 403 on every endpoint
179
179
  - These are **foundational** — without them, ALL subsequent features (frontend, API, tests) are useless
180
180
 
181
+ **POST-CHECK: Section route/permission completeness (BLOCKING — when sections defined)**
182
+
183
+ After generating section seed data, verify completeness:
184
+
185
+ ```bash
186
+ # 1. Verify every NavigationSection seed route has a frontend route match
187
+ SECTION_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -exec grep -l 'GetSectionEntries' {} \; 2>/dev/null)
188
+ if [ -n "$SECTION_SEED_FILES" ]; then
189
+ echo "Sections defined — verifying frontend route matches..."
190
+ SECTION_ROUTES=$(grep -ohP 'Route\s*=\s*ToKebabCase\(\$?"([^"]+)"\)' $SECTION_SEED_FILES 2>/dev/null | grep -i section)
191
+ APP_TSX="$WEB_SRC/App.tsx"
192
+ if [ -n "$APP_TSX" ] && [ -n "$SECTION_ROUTES" ]; then
193
+ while IFS= read -r ROUTE; do
194
+ ROUTE_PATH=$(echo "$ROUTE" | grep -oP '"([^"]+)"' | tr -d '"')
195
+ if [ -n "$ROUTE_PATH" ] && ! grep -q "$ROUTE_PATH" "$APP_TSX" 2>/dev/null; then
196
+ echo "WARNING: Section route '$ROUTE_PATH' not found in App.tsx — add frontend route"
197
+ fi
198
+ done <<< "$SECTION_ROUTES"
199
+ fi
200
+
201
+ # 2. Verify section-level permissions exist when sections are defined
202
+ PERM_SEED=$(find . -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
203
+ if [ -n "$PERM_SEED" ]; then
204
+ for PS in $PERM_SEED; do
205
+ HAS_SECTION_PERMS=$(grep -c 'sectionCode\|SectionPascal\|Level = PermissionLevel.Section' "$PS" 2>/dev/null)
206
+ if [ "$HAS_SECTION_PERMS" -eq 0 ]; then
207
+ echo "BLOCKING: Sections defined but PermissionsSeedData missing section-level permissions: $PS"
208
+ echo "Fix: Add section-level permission GUIDs and entries per core-seed-data.md Step C2"
209
+ exit 1
210
+ fi
211
+ done
212
+ fi
213
+
214
+ # 3. Verify section-level role mappings exist
215
+ ROLE_SEED=$(find . -path "*/Seeding/Data/*" -name "RolesSeedData.cs" 2>/dev/null)
216
+ if [ -n "$ROLE_SEED" ]; then
217
+ for RS in $ROLE_SEED; do
218
+ HAS_SECTION_ROLES=$(grep -c 'sectionCode' "$RS" 2>/dev/null)
219
+ if [ "$HAS_SECTION_ROLES" -eq 0 ]; then
220
+ echo "BLOCKING: Sections defined but RolesSeedData missing section-level role mappings: $RS"
221
+ echo "Fix: Add section-level role-permission entries per core-seed-data.md section 5"
222
+ exit 1
223
+ fi
224
+ done
225
+ fi
226
+ fi
227
+ ```
228
+
229
+ **POST-CHECK: Navigation section route CRUD suffix detection (BLOCKING)**
230
+
231
+ After generating NavigationSeedData files with sections, verify no route contains `/list` or `/detail` suffixes:
232
+
233
+ ```bash
234
+ # Detect CRUD suffixes in navigation section routes
235
+ CRUD_ROUTES=$(grep -rn 'Route.*=.*\/list\b\|Route.*=.*\/detail' Infrastructure/Persistence/Seeding/Data/ 2>/dev/null | grep -v '//.*Route')
236
+
237
+ if [ -n "$CRUD_ROUTES" ]; then
238
+ echo "BLOCKING: Navigation section routes contain CRUD suffixes"
239
+ echo "Convention:"
240
+ echo " - 'list' section route = module route (NO /list suffix)"
241
+ echo " - 'detail' section route = module route + /:id (NOT /detail/:id)"
242
+ echo " - Other sections = module route + /{section-kebab}"
243
+ echo ""
244
+ echo "Found:"
245
+ echo "$CRUD_ROUTES"
246
+ echo ""
247
+ echo "Fix: Remove /list suffix (use module route), replace /detail/:id with /:id"
248
+ exit 1
249
+ fi
250
+ ```
251
+
252
+ **Why this matters:**
253
+ - React Router defines NO `/list` child route — the module route IS the list view
254
+ - React Router uses `/:id` for detail views, NOT `/detail/:id`
255
+ - Mismatch causes 404 on navigation menu click
256
+
257
+ **POST-CHECK: Permission path segment count (WARNING — when permissions defined)**
258
+
259
+ ```bash
260
+ PERM_FILES=$(find Infrastructure/Persistence/Seeding/Data/ -name "PermissionsSeedData.cs" 2>/dev/null)
261
+ if [ -n "$PERM_FILES" ]; then
262
+ MALFORMED=$(grep -oP 'Path\s*=\s*"([^"]+)"' $PERM_FILES | grep -oP '"[^"]+"' | tr -d '"' | while read -r path; do
263
+ DOTS=$(echo "$path" | tr -cd '.' | wc -c)
264
+ if echo "$path" | grep -qP '\.\*$'; then continue; fi
265
+ if [ "$DOTS" -lt 3 ] || [ "$DOTS" -gt 5 ]; then
266
+ echo " Unexpected segment count ($((DOTS+1))): $path"
267
+ fi
268
+ done)
269
+ if [ -n "$MALFORMED" ]; then
270
+ echo "WARNING: Permission paths with unexpected segment count:"
271
+ echo "$MALFORMED"
272
+ echo " Expected: 4 segments (module-level) or 5 segments (section-level)"
273
+ fi
274
+ fi
275
+ ```
276
+
181
277
  **POST-CHECK: DefaultTenantId cross-schema FK validation (WARNING)**
182
278
 
183
279
  After generating SeedConstants.cs and DevDataSeeder, verify that `DefaultTenantId` is not a phantom GUID that doesn't exist in `core.tenant_Tenants`:
@@ -602,12 +698,24 @@ if [ -n "$SERVICE_FILES" ]; then
602
698
  BAD_PATTERN=$(grep -Pn 'TenantId!\s*\.Value|TenantId!\s*\.ToString|\.TenantId!' $SERVICE_FILES 2>/dev/null)
603
699
  if [ -n "$BAD_PATTERN" ]; then
604
700
  echo "BLOCKING: TenantId!.Value causes 500 when tenant context is missing"
605
- echo "Fix: var tenantId = _currentTenant.TenantId ?? throw new UnauthorizedAccessException(\"Tenant context is required\");"
701
+ echo "Fix: var tenantId = _currentTenant.TenantId ?? throw new TenantContextRequiredException();"
702
+ echo "NEVER use UnauthorizedAccessException for tenant context — it returns 401 which clears the frontend token."
606
703
  echo "$BAD_PATTERN"
607
704
  exit 1
608
705
  fi
609
706
  fi
610
707
 
708
+ # POST-CHECK: Services must NOT use UnauthorizedAccessException for tenant context (causes token clearing)
709
+ if [ -n "$SERVICE_FILES" ]; then
710
+ BAD_UNAUTH=$(grep -Pn 'UnauthorizedAccessException.*[Tt]enant' $SERVICE_FILES 2>/dev/null)
711
+ if [ -n "$BAD_UNAUTH" ]; then
712
+ echo "BLOCKING: Services use UnauthorizedAccessException for tenant context — causes 401 which clears the frontend token"
713
+ echo "$BAD_UNAUTH"
714
+ echo "Fix: var tenantId = _currentTenant.TenantId ?? throw new TenantContextRequiredException();"
715
+ exit 1
716
+ fi
717
+ fi
718
+
611
719
  # POST-CHECK: IAuditableEntity + Validator pairing
612
720
  ENTITY_FILES=$(find src/ -path "*/Domain/Entities/Business/*" -name "*.cs" 2>/dev/null)
613
721
  if [ -n "$ENTITY_FILES" ]; then