@atlashub/smartstack-cli 3.28.0 → 3.30.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 (44) 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 +40 -11
  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/team-orchestration.md +57 -23
  29. package/templates/skills/business-analyse/references/ui-resource-cards.md +3 -3
  30. package/templates/skills/business-analyse/references/validation-checklist.md +21 -3
  31. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +33 -5
  32. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +12 -0
  33. package/templates/skills/business-analyse/steps/step-03b-ui.md +14 -2
  34. package/templates/skills/business-analyse/steps/step-03c-compile.md +17 -9
  35. package/templates/skills/business-analyse/steps/step-03d-validate.md +42 -2
  36. package/templates/skills/business-analyse/steps/step-04b-analyze.md +5 -3
  37. package/templates/skills/business-analyse/steps/step-05a-handoff.md +23 -15
  38. package/templates/skills/business-analyse/templates/tpl-handoff.md +10 -8
  39. package/templates/skills/business-analyse/templates/tpl-progress.md +7 -6
  40. package/templates/skills/ralph-loop/references/category-rules.md +50 -6
  41. package/templates/skills/ralph-loop/references/compact-loop.md +16 -1
  42. package/templates/skills/ralph-loop/references/core-seed-data.md +158 -38
  43. package/templates/skills/ralph-loop/references/task-transform-legacy.md +3 -3
  44. package/templates/skills/ralph-loop/steps/step-02-execute.md +109 -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