@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.
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/templates/project/api.ts.template +4 -2
- package/templates/project/appsettings.json.template +1 -1
- package/templates/skills/apex/_shared.md +13 -0
- package/templates/skills/apex/references/post-checks.md +228 -6
- package/templates/skills/apex/references/smartstack-api.md +67 -17
- package/templates/skills/apex/references/smartstack-frontend.md +41 -1
- package/templates/skills/apex/references/smartstack-layers.md +40 -10
- package/templates/skills/apex/steps/step-02-plan.md +16 -11
- package/templates/skills/apex/steps/step-03-execute.md +6 -0
- package/templates/skills/apex/steps/step-04-examine.md +4 -2
- package/templates/skills/application/references/frontend-verification.md +26 -1
- package/templates/skills/application/steps/step-03-roles.md +1 -1
- package/templates/skills/application/steps/step-05-frontend.md +24 -8
- package/templates/skills/application/templates-frontend.md +41 -22
- package/templates/skills/application/templates-seed.md +53 -16
- package/templates/skills/business-analyse/SKILL.md +4 -2
- package/templates/skills/business-analyse/_shared.md +17 -4
- package/templates/skills/business-analyse/react/schema.md +1 -1
- package/templates/skills/business-analyse/references/agent-module-prompt.md +40 -11
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +4 -3
- package/templates/skills/business-analyse/references/deploy-modes.md +1 -1
- package/templates/skills/business-analyse/references/handoff-file-templates.md +4 -4
- package/templates/skills/business-analyse/references/robustness-checks.md +12 -9
- package/templates/skills/business-analyse/references/spec-auto-inference.md +3 -3
- package/templates/skills/business-analyse/references/team-orchestration.md +57 -23
- package/templates/skills/business-analyse/references/ui-resource-cards.md +3 -3
- package/templates/skills/business-analyse/references/validation-checklist.md +21 -3
- package/templates/skills/business-analyse/schemas/sections/specification-schema.json +33 -5
- package/templates/skills/business-analyse/steps/step-03a2-analysis.md +12 -0
- package/templates/skills/business-analyse/steps/step-03b-ui.md +14 -2
- package/templates/skills/business-analyse/steps/step-03c-compile.md +17 -9
- package/templates/skills/business-analyse/steps/step-03d-validate.md +42 -2
- package/templates/skills/business-analyse/steps/step-04b-analyze.md +5 -3
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +23 -15
- package/templates/skills/business-analyse/templates/tpl-handoff.md +10 -8
- package/templates/skills/business-analyse/templates/tpl-progress.md +7 -6
- package/templates/skills/ralph-loop/references/category-rules.md +50 -6
- package/templates/skills/ralph-loop/references/compact-loop.md +16 -1
- package/templates/skills/ralph-loop/references/core-seed-data.md +158 -38
- package/templates/skills/ralph-loop/references/task-transform-legacy.md +3 -3
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
965
|
-
.
|
|
966
|
-
if (exists) return;
|
|
1054
|
+
var existingApp = await context.NavigationApplications
|
|
1055
|
+
.FirstOrDefaultAsync(a => a.Code == appEntry.Code, ct);
|
|
967
1056
|
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
984
|
-
|
|
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
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
mod1Entry
|
|
995
|
-
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
1106
|
-
//
|
|
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
|
-
|
|
1233
|
+
|
|
1234
|
+
if (role == null)
|
|
1126
1235
|
{
|
|
1127
|
-
|
|
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
|
|
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
|