@atlashub/smartstack-cli 3.21.0 → 3.23.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 +17 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +155 -162
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/skills/apex/SKILL.md +21 -0
- package/templates/skills/apex/references/smartstack-api.md +481 -0
- package/templates/skills/apex/references/smartstack-layers.md +85 -15
- package/templates/skills/apex/steps/step-00-init.md +27 -14
- package/templates/skills/apex/steps/step-01-analyze.md +18 -0
- package/templates/skills/apex/steps/step-03-execute.md +8 -6
- package/templates/skills/apex/steps/step-04-validate.md +92 -0
- package/templates/skills/apex/steps/step-07-tests.md +29 -5
- package/templates/skills/application/references/application-roles-template.md +2 -2
- package/templates/skills/application/steps/step-05-frontend.md +40 -35
- package/templates/skills/application/templates-frontend.md +64 -36
- package/templates/skills/business-analyse/html/ba-interactive.html +80 -6
- package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +38 -6
- package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +42 -0
- package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -0
- package/templates/skills/business-analyse/references/deploy-data-build.md +5 -3
- package/templates/skills/business-analyse/references/handoff-file-templates.md +2 -1
- package/templates/skills/business-analyse/references/naming-conventions.md +245 -0
- package/templates/skills/business-analyse/references/validate-incremental-html.md +26 -4
- package/templates/skills/business-analyse/references/validation-checklist.md +31 -11
- package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -0
- package/templates/skills/business-analyse/steps/step-03b-ui.md +59 -0
- package/templates/skills/business-analyse/steps/step-03c-compile.md +114 -0
- package/templates/skills/business-analyse/steps/step-03d-validate.md +144 -22
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +114 -2
- package/templates/skills/business-analyse/steps/step-05b-deploy.md +28 -0
- package/templates/skills/ralph-loop/references/category-rules.md +5 -2
- package/templates/skills/ralph-loop/references/compact-loop.md +52 -1
- package/templates/skills/ralph-loop/references/core-seed-data.md +232 -21
- package/templates/skills/ralph-loop/steps/step-01-task.md +36 -4
- package/templates/skills/ralph-loop/steps/step-02-execute.md +81 -0
|
@@ -53,6 +53,171 @@ if (!navRoute) {
|
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
|
+
## 1b. NavigationApplicationSeedData.cs (ONCE per application)
|
|
57
|
+
|
|
58
|
+
**File:** `Infrastructure/Persistence/Seeding/Data/NavigationApplicationSeedData.cs`
|
|
59
|
+
|
|
60
|
+
> **MANDATORY:** This file MUST be created BEFORE any module seed data.
|
|
61
|
+
> Without it, modules have no parent ApplicationId and ApplicationRolesSeedData has no GUID reference.
|
|
62
|
+
> This file is created **ONCE per application** (not per module).
|
|
63
|
+
|
|
64
|
+
### Data Source
|
|
65
|
+
|
|
66
|
+
From `seedDataCore.navigationApplications[0]` in feature.json (generated by BA step-05a):
|
|
67
|
+
|
|
68
|
+
| Placeholder | Source |
|
|
69
|
+
|-------------|--------|
|
|
70
|
+
| `{appCode}` | `navigationApplications[0].code` |
|
|
71
|
+
| `{appLabel_xx}` | `navigationApplications[0].labels.xx` (fr, en, it, de) |
|
|
72
|
+
| `{appDesc_xx}` | `navigationApplications[0].description.xx` |
|
|
73
|
+
| `{appIcon}` | `navigationApplications[0].icon` |
|
|
74
|
+
| `{contextCode}` | `navigationApplications[0].context` or `_seedDataMeta.contextCode` |
|
|
75
|
+
|
|
76
|
+
### GUID Generation Rule
|
|
77
|
+
|
|
78
|
+
```csharp
|
|
79
|
+
// Deterministic GUID from context + application code
|
|
80
|
+
public static readonly Guid ApplicationId =
|
|
81
|
+
GenerateDeterministicGuid("navigation-application-{contextCode}.{appCode}");
|
|
82
|
+
// Example: GenerateDeterministicGuid("navigation-application-business.humanresources")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Template
|
|
86
|
+
|
|
87
|
+
```csharp
|
|
88
|
+
using SmartStack.Domain.Navigation;
|
|
89
|
+
|
|
90
|
+
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data;
|
|
91
|
+
|
|
92
|
+
/// <summary>
|
|
93
|
+
/// Navigation seed data for {AppLabel_en} application.
|
|
94
|
+
/// Consumed by IClientSeedDataProvider at application startup.
|
|
95
|
+
/// Created ONCE per application — modules reference ApplicationId as parent.
|
|
96
|
+
/// </summary>
|
|
97
|
+
public static class NavigationApplicationSeedData
|
|
98
|
+
{
|
|
99
|
+
// Deterministic GUID for this application
|
|
100
|
+
public static readonly Guid ApplicationId =
|
|
101
|
+
GenerateDeterministicGuid("navigation-application-{contextCode}.{appCode}");
|
|
102
|
+
|
|
103
|
+
/// <summary>
|
|
104
|
+
/// Returns navigation application entry for seeding into core.nav_Applications.
|
|
105
|
+
/// </summary>
|
|
106
|
+
public static NavigationApplicationSeedEntry GetApplicationEntry(Guid contextId)
|
|
107
|
+
{
|
|
108
|
+
return new NavigationApplicationSeedEntry
|
|
109
|
+
{
|
|
110
|
+
Id = ApplicationId,
|
|
111
|
+
ContextId = contextId,
|
|
112
|
+
Code = "{appCode}",
|
|
113
|
+
Label = "{appLabel_en}",
|
|
114
|
+
Description = "{appDesc_en}",
|
|
115
|
+
Icon = "{appIcon}", // Lucide React icon name
|
|
116
|
+
IconType = IconType.Lucide,
|
|
117
|
+
Route = ToKebabCase("/{contextCode}/{appCode}"),
|
|
118
|
+
DisplayOrder = 1,
|
|
119
|
+
IsActive = true
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// <summary>
|
|
124
|
+
/// Returns 4-language translations for this application.
|
|
125
|
+
/// </summary>
|
|
126
|
+
public static IEnumerable<NavigationTranslationSeedEntry> GetTranslationEntries()
|
|
127
|
+
{
|
|
128
|
+
var appId = ApplicationId;
|
|
129
|
+
return new[]
|
|
130
|
+
{
|
|
131
|
+
new NavigationTranslationSeedEntry
|
|
132
|
+
{
|
|
133
|
+
EntityType = NavigationEntityType.Application,
|
|
134
|
+
EntityId = appId,
|
|
135
|
+
LanguageCode = "fr",
|
|
136
|
+
Label = "{appLabel_fr}",
|
|
137
|
+
Description = "{appDesc_fr}"
|
|
138
|
+
},
|
|
139
|
+
new NavigationTranslationSeedEntry
|
|
140
|
+
{
|
|
141
|
+
EntityType = NavigationEntityType.Application,
|
|
142
|
+
EntityId = appId,
|
|
143
|
+
LanguageCode = "en",
|
|
144
|
+
Label = "{appLabel_en}",
|
|
145
|
+
Description = "{appDesc_en}"
|
|
146
|
+
},
|
|
147
|
+
new NavigationTranslationSeedEntry
|
|
148
|
+
{
|
|
149
|
+
EntityType = NavigationEntityType.Application,
|
|
150
|
+
EntityId = appId,
|
|
151
|
+
LanguageCode = "it",
|
|
152
|
+
Label = "{appLabel_it}",
|
|
153
|
+
Description = "{appDesc_it}"
|
|
154
|
+
},
|
|
155
|
+
new NavigationTranslationSeedEntry
|
|
156
|
+
{
|
|
157
|
+
EntityType = NavigationEntityType.Application,
|
|
158
|
+
EntityId = appId,
|
|
159
|
+
LanguageCode = "de",
|
|
160
|
+
Label = "{appLabel_de}",
|
|
161
|
+
Description = "{appDesc_de}"
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private static Guid GenerateDeterministicGuid(string seed)
|
|
167
|
+
{
|
|
168
|
+
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
169
|
+
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
170
|
+
return new Guid(hash.Take(16).ToArray());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// <summary>
|
|
174
|
+
/// Converts PascalCase route segments to kebab-case for web URLs.
|
|
175
|
+
/// </summary>
|
|
176
|
+
private static string ToKebabCase(string route)
|
|
177
|
+
{
|
|
178
|
+
if (string.IsNullOrEmpty(route)) return route;
|
|
179
|
+
|
|
180
|
+
var segments = route.Split('/');
|
|
181
|
+
var kebabSegments = new List<string>();
|
|
182
|
+
|
|
183
|
+
foreach (var segment in segments)
|
|
184
|
+
{
|
|
185
|
+
if (string.IsNullOrEmpty(segment))
|
|
186
|
+
{
|
|
187
|
+
kebabSegments.Add(segment);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
var kebab = System.Text.RegularExpressions.Regex
|
|
192
|
+
.Replace(segment, "([a-z])([A-Z])", "$1-$2")
|
|
193
|
+
.ToLowerInvariant();
|
|
194
|
+
kebabSegments.Add(kebab);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return string.Join("/", kebabSegments);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// <summary>Seed entry DTO for navigation application.</summary>
|
|
202
|
+
public class NavigationApplicationSeedEntry
|
|
203
|
+
{
|
|
204
|
+
public Guid Id { get; init; }
|
|
205
|
+
public Guid ContextId { get; init; }
|
|
206
|
+
public string Code { get; init; } = null!;
|
|
207
|
+
public string Label { get; init; } = null!;
|
|
208
|
+
public string? Description { get; init; }
|
|
209
|
+
public string? Icon { get; init; }
|
|
210
|
+
public IconType IconType { get; init; }
|
|
211
|
+
public string? Route { get; init; }
|
|
212
|
+
public int DisplayOrder { get; init; }
|
|
213
|
+
public bool IsActive { get; init; }
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Replace placeholders** with values from `seedDataCore.navigationApplications[0]`.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
56
221
|
## 2. NavigationModuleSeedData.cs
|
|
57
222
|
|
|
58
223
|
**File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/NavigationModuleSeedData.cs`
|
|
@@ -104,7 +269,7 @@ public static class {ModulePascal}NavigationSeedData
|
|
|
104
269
|
Description = "{desc_en}",
|
|
105
270
|
Icon = "{icon}", // Lucide React icon name
|
|
106
271
|
IconType = IconType.Lucide,
|
|
107
|
-
Route = "/{contextCode}/{appCode}/{moduleCode}",
|
|
272
|
+
Route = ToKebabCase($"/{contextCode}/{appCode}/{moduleCode}"),
|
|
108
273
|
DisplayOrder = {displayOrder},
|
|
109
274
|
IsActive = true
|
|
110
275
|
};
|
|
@@ -159,6 +324,35 @@ public static class {ModulePascal}NavigationSeedData
|
|
|
159
324
|
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
160
325
|
return new Guid(hash.Take(16).ToArray());
|
|
161
326
|
}
|
|
327
|
+
|
|
328
|
+
/// <summary>
|
|
329
|
+
/// Converts PascalCase route segments to kebab-case for web URLs.
|
|
330
|
+
/// Example: /business/HumanResources/TimeManagement → /business/human-resources/time-management
|
|
331
|
+
/// </summary>
|
|
332
|
+
private static string ToKebabCase(string route)
|
|
333
|
+
{
|
|
334
|
+
if (string.IsNullOrEmpty(route)) return route;
|
|
335
|
+
|
|
336
|
+
var segments = route.Split('/');
|
|
337
|
+
var kebabSegments = new List<string>();
|
|
338
|
+
|
|
339
|
+
foreach (var segment in segments)
|
|
340
|
+
{
|
|
341
|
+
if (string.IsNullOrEmpty(segment))
|
|
342
|
+
{
|
|
343
|
+
kebabSegments.Add(segment);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Convert PascalCase to kebab-case: HumanResources → human-resources
|
|
348
|
+
var kebab = System.Text.RegularExpressions.Regex
|
|
349
|
+
.Replace(segment, "([a-z])([A-Z])", "$1-$2")
|
|
350
|
+
.ToLowerInvariant();
|
|
351
|
+
kebabSegments.Add(kebab);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return string.Join("/", kebabSegments);
|
|
355
|
+
}
|
|
162
356
|
}
|
|
163
357
|
|
|
164
358
|
/// <summary>Seed entry DTO for navigation module.</summary>
|
|
@@ -236,7 +430,7 @@ public static IEnumerable<NavigationSectionSeedEntry> GetSectionEntries(Guid mod
|
|
|
236
430
|
Description = "{section1_desc_en}",
|
|
237
431
|
Icon = "{section1_icon}",
|
|
238
432
|
IconType = IconType.Lucide,
|
|
239
|
-
Route = "/{contextCode}/{appCode}/{moduleCode}/{section1Code}",
|
|
433
|
+
Route = ToKebabCase($"/{contextCode}/{appCode}/{moduleCode}/{section1Code}"),
|
|
240
434
|
DisplayOrder = {section1_sort},
|
|
241
435
|
IsActive = true
|
|
242
436
|
}
|
|
@@ -325,7 +519,7 @@ public static IEnumerable<NavigationResourceSeedEntry> GetResourceEntries(Guid s
|
|
|
325
519
|
Code = "{resource1Code}",
|
|
326
520
|
Label = "{resource1_label_en}",
|
|
327
521
|
EntityType = "{resource1_entity}",
|
|
328
|
-
Route = "/{contextCode}/{appCode}/{moduleCode}/{section1Code}/{resource1Code}",
|
|
522
|
+
Route = ToKebabCase($"/{contextCode}/{appCode}/{moduleCode}/{section1Code}/{resource1Code}"),
|
|
329
523
|
DisplayOrder = 1
|
|
330
524
|
}
|
|
331
525
|
// Repeat for each resource in this section...
|
|
@@ -559,8 +753,8 @@ namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data;
|
|
|
559
753
|
/// </summary>
|
|
560
754
|
public static class ApplicationRolesSeedData
|
|
561
755
|
{
|
|
562
|
-
// Application ID from NavigationApplicationSeedData
|
|
563
|
-
|
|
756
|
+
// Application ID from NavigationApplicationSeedData (deterministic GUID)
|
|
757
|
+
public static readonly Guid ApplicationId = NavigationApplicationSeedData.ApplicationId;
|
|
564
758
|
|
|
565
759
|
public static readonly Guid AdminRoleId = GenerateRoleGuid("admin");
|
|
566
760
|
public static readonly Guid ManagerRoleId = GenerateRoleGuid("manager");
|
|
@@ -738,30 +932,30 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
738
932
|
|
|
739
933
|
public async Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct)
|
|
740
934
|
{
|
|
741
|
-
// --- Application ---
|
|
935
|
+
// --- Application (from NavigationApplicationSeedData) ---
|
|
936
|
+
var appEntry = NavigationApplicationSeedData.GetApplicationEntry(Guid.Empty); // contextId resolved below
|
|
742
937
|
var exists = await context.NavigationApplications
|
|
743
|
-
.AnyAsync(a => a.Code ==
|
|
938
|
+
.AnyAsync(a => a.Code == appEntry.Code, ct);
|
|
744
939
|
if (exists) return;
|
|
745
940
|
|
|
746
941
|
var parentContext = await context.NavigationContexts
|
|
747
942
|
.FirstAsync(c => c.Code == "{contextCode}", ct);
|
|
748
943
|
|
|
944
|
+
// Re-get entry with resolved contextId
|
|
945
|
+
appEntry = NavigationApplicationSeedData.GetApplicationEntry(parentContext.Id);
|
|
749
946
|
var app = NavigationApplication.Create(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
947
|
+
appEntry.ContextId, appEntry.Code, appEntry.Label,
|
|
948
|
+
appEntry.Description, appEntry.Icon, appEntry.IconType,
|
|
949
|
+
appEntry.Route, appEntry.DisplayOrder);
|
|
753
950
|
context.NavigationApplications.Add(app);
|
|
754
951
|
await ((DbContext)context).SaveChangesAsync(ct);
|
|
755
952
|
|
|
756
|
-
// --- Application translations (4 languages) ---
|
|
757
|
-
var
|
|
953
|
+
// --- Application translations (4 languages, from NavigationApplicationSeedData) ---
|
|
954
|
+
foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
|
|
758
955
|
{
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
NavigationTranslation.Create(NavigationEntityType.Application, app.Id, "de", "{appLabel_de}", "{appDesc_de}")
|
|
763
|
-
};
|
|
764
|
-
foreach (var t in appTranslations) context.NavigationTranslations.Add(t);
|
|
956
|
+
context.NavigationTranslations.Add(
|
|
957
|
+
NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
|
|
958
|
+
}
|
|
765
959
|
await ((DbContext)context).SaveChangesAsync(ct);
|
|
766
960
|
|
|
767
961
|
// --- Modules ---
|
|
@@ -926,7 +1120,8 @@ When processing multiple modules in the same ralph-loop run:
|
|
|
926
1120
|
|
|
927
1121
|
### Module 1 (first): Creates everything from scratch
|
|
928
1122
|
|
|
929
|
-
|
|
1123
|
+
0. `NavigationApplicationSeedData.cs` (**application-level**, once per app — FIRST FILE, provides ApplicationId)
|
|
1124
|
+
1. `ApplicationRolesSeedData.cs` (application-level, once per app — references NavigationApplicationSeedData.ApplicationId)
|
|
930
1125
|
2. `{Module1}NavigationSeedData.cs` (module + sections + resources + all translations)
|
|
931
1126
|
3. `{Module1}PermissionsSeedData.cs`
|
|
932
1127
|
4. `{Module1}RolesSeedData.cs`
|
|
@@ -935,6 +1130,7 @@ When processing multiple modules in the same ralph-loop run:
|
|
|
935
1130
|
|
|
936
1131
|
### Module 2+ (subsequent): Append to existing provider
|
|
937
1132
|
|
|
1133
|
+
0. `NavigationApplicationSeedData.cs` (already exists — skip)
|
|
938
1134
|
1. `ApplicationRolesSeedData.cs` (already exists — skip)
|
|
939
1135
|
2. `{Module2}NavigationSeedData.cs` (new file — include sections + resources if defined in feature.json)
|
|
940
1136
|
3. `{Module2}PermissionsSeedData.cs` (new file)
|
|
@@ -942,7 +1138,7 @@ When processing multiple modules in the same ralph-loop run:
|
|
|
942
1138
|
5. `{AppPascalName}SeedDataProvider.cs` (**modify** — add using, add entries in Navigation/Permissions/RolePermissions methods, **including section/resource seeding for the new module**)
|
|
943
1139
|
6. DI registration (already exists — skip)
|
|
944
1140
|
|
|
945
|
-
**Detection:** Check if `{AppPascalName}SeedDataProvider.cs` exists. If yes, READ it and ADD the new module's entries to the appropriate methods (Navigation, Permissions, RolePermissions). Do NOT modify SeedRolesAsync().
|
|
1141
|
+
**Detection:** Check if `{AppPascalName}SeedDataProvider.cs` exists. If yes, READ it and ADD the new module's entries to the appropriate methods (Navigation, Permissions, RolePermissions). Do NOT modify SeedRolesAsync() or the Application creation in SeedNavigationAsync().
|
|
946
1142
|
|
|
947
1143
|
### Section/Resource Conditionality
|
|
948
1144
|
|
|
@@ -960,6 +1156,14 @@ Sections and resources are **optional per module**. When processing a module's f
|
|
|
960
1156
|
|
|
961
1157
|
Before marking the task as completed, verify ALL:
|
|
962
1158
|
|
|
1159
|
+
**Application-Level (FIRST — before modules):**
|
|
1160
|
+
- [ ] `NavigationApplicationSeedData.cs` created (once per application, at `Infrastructure/Persistence/Seeding/Data/`)
|
|
1161
|
+
- [ ] Application GUID is deterministic (SHA256 of `"navigation-application-{contextCode}.{appCode}"`)
|
|
1162
|
+
- [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application)
|
|
1163
|
+
- [ ] `IClientSeedDataProvider.SeedNavigationAsync()` uses `NavigationApplicationSeedData` (NO hardcoded `{appLabel_en}` / `{appIcon}` placeholders)
|
|
1164
|
+
- [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (NO `{ApplicationGuid}` placeholder)
|
|
1165
|
+
|
|
1166
|
+
**Module-Level:**
|
|
963
1167
|
- [ ] Deterministic GUIDs (NEVER `Guid.NewGuid()`) — SHA256 of path
|
|
964
1168
|
- [ ] 4 languages for each navigation entity (fr, en, it, de)
|
|
965
1169
|
- [ ] `ApplicationRolesSeedData.cs` created (once per application)
|
|
@@ -969,7 +1173,7 @@ Before marking the task as completed, verify ALL:
|
|
|
969
1173
|
- [ ] MCP `generate_permissions` called (or fallback used)
|
|
970
1174
|
- [ ] Role-permission mappings assigned (Admin, Manager, Contributor, Viewer)
|
|
971
1175
|
- [ ] `IClientSeedDataProvider` generated with 4 methods (Navigation, Roles, Permissions, RolePermissions)
|
|
972
|
-
- [ ] Execution order: Navigation (modules → sections → resources) → Roles → Permissions → RolePermissions
|
|
1176
|
+
- [ ] Execution order: Navigation (application → modules → sections → resources) → Roles → Permissions → RolePermissions
|
|
973
1177
|
- [ ] Each Seed method is idempotent (checks existence before inserting)
|
|
974
1178
|
- [ ] Factory methods used throughout (NEVER `new Entity()`)
|
|
975
1179
|
- [ ] `SaveChangesAsync` called per group (Navigation → Roles → Permissions → RolePermissions)
|
|
@@ -981,6 +1185,13 @@ Before marking the task as completed, verify ALL:
|
|
|
981
1185
|
- [ ] NO `Enum.Parse<PermissionAction>` usage anywhere in seeding code (use typed enum directly)
|
|
982
1186
|
- [ ] ALL PermissionAction values are from the valid enum: Access, Read, Create, Update, Delete, Export, Import, Approve, Reject, Assign, Execute
|
|
983
1187
|
|
|
1188
|
+
**Seed Data Integrity (BLOCKING — run AFTER `dotnet build`):**
|
|
1189
|
+
- [ ] Application startup test: `dotnet run --urls http://localhost:0 --environment Development` exits without seed data exceptions
|
|
1190
|
+
- [ ] Verify `nav_Applications` has entry for `{appCode}` (query or startup log)
|
|
1191
|
+
- [ ] Verify `nav_Modules` has entries for each module (count matches feature.json modules)
|
|
1192
|
+
- [ ] Verify `auth_Roles` has 4 application-scoped roles (admin, manager, contributor, viewer)
|
|
1193
|
+
- [ ] Verify `auth_Permissions` has entries for each module (wildcard + CRUD)
|
|
1194
|
+
|
|
984
1195
|
**If ANY check fails, the task status = 'failed'.**
|
|
985
1196
|
|
|
986
1197
|
---
|
|
@@ -186,7 +186,9 @@ Initialize `.ralph/progress.txt`:
|
|
|
186
186
|
> This check prevents the "no frontend generated" failure mode.
|
|
187
187
|
|
|
188
188
|
```javascript
|
|
189
|
-
|
|
189
|
+
// CRITICAL: seedData MUST be in the required list — without it, nav_Applications/auth_Roles/auth_Permissions
|
|
190
|
+
// are not seeded, and frontend/test tasks execute against an empty database.
|
|
191
|
+
const REQUIRED_CATEGORIES = ['domain', 'infrastructure', 'application', 'api', 'seedData', 'frontend', 'test'];
|
|
190
192
|
const presentCategories = new Set(prd.tasks.map(t => t.category));
|
|
191
193
|
const missingCategories = REQUIRED_CATEGORIES.filter(c => !presentCategories.has(c));
|
|
192
194
|
|
|
@@ -199,19 +201,38 @@ if (missingCategories.length > 0) {
|
|
|
199
201
|
return isNaN(num) ? 0 : num;
|
|
200
202
|
}));
|
|
201
203
|
const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'GUARD-';
|
|
202
|
-
const
|
|
204
|
+
const lastApiTask = prd.tasks.filter(t => t.category === 'api').pop()?.id || prd.tasks[prd.tasks.length - 1]?.id;
|
|
205
|
+
const lastSeedDataTask = prd.tasks.filter(t => t.category === 'seedData').pop()?.id;
|
|
206
|
+
|
|
207
|
+
// DEPENDENCY CHAIN: domain → infrastructure → application → api → seedData → frontend → test
|
|
208
|
+
// Frontend MUST depend on seedData (not just API) — otherwise navigation/permissions are empty
|
|
209
|
+
const frontendDep = lastSeedDataTask || lastApiTask;
|
|
203
210
|
|
|
204
211
|
for (const cat of missingCategories) {
|
|
205
212
|
maxIdNum++;
|
|
206
213
|
const taskId = `${prefix}${String(maxIdNum).padStart(3, '0')}`;
|
|
214
|
+
|
|
215
|
+
// Determine dependencies based on category order
|
|
216
|
+
let deps;
|
|
217
|
+
if (cat === 'seedData') {
|
|
218
|
+
deps = [lastApiTask]; // seedData depends on API (infrastructure must exist first)
|
|
219
|
+
} else if (cat === 'frontend') {
|
|
220
|
+
deps = [frontendDep]; // frontend depends on seedData (navigation must be seeded)
|
|
221
|
+
} else if (cat === 'test') {
|
|
222
|
+
deps = [frontendDep]; // test depends on seedData too
|
|
223
|
+
} else {
|
|
224
|
+
deps = [];
|
|
225
|
+
}
|
|
226
|
+
|
|
207
227
|
const guardrailTask = {
|
|
208
228
|
id: taskId,
|
|
209
229
|
description: `[GUARDRAIL] Generate ${cat} layer for ${prd.project?.module || 'module'}`,
|
|
210
230
|
status: 'pending',
|
|
211
231
|
category: cat,
|
|
212
|
-
dependencies:
|
|
232
|
+
dependencies: deps,
|
|
213
233
|
acceptance_criteria: [
|
|
214
|
-
cat === '
|
|
234
|
+
cat === 'seedData' ? 'NavigationApplicationSeedData + NavigationModuleSeedData + PermissionsSeedData + ApplicationRolesSeedData created, IClientSeedDataProvider wired, startup test passes' :
|
|
235
|
+
cat === 'frontend' ? 'React pages created via MCP scaffold_api_client + scaffold_routes, wired to App.tsx, navigation seed data verified in DB' :
|
|
215
236
|
cat === 'test' ? 'Unit + Integration test projects created, scaffold_tests MCP called, dotnet test passes' :
|
|
216
237
|
`${cat} layer fully implemented per category-rules.md`
|
|
217
238
|
],
|
|
@@ -220,6 +241,17 @@ if (missingCategories.length > 0) {
|
|
|
220
241
|
};
|
|
221
242
|
prd.tasks.push(guardrailTask);
|
|
222
243
|
console.log(` → Injected guardrail: [${taskId}] ${cat}`);
|
|
244
|
+
|
|
245
|
+
// Update frontendDep if we just injected seedData (frontend guardrail should depend on it)
|
|
246
|
+
if (cat === 'seedData') {
|
|
247
|
+
// Re-resolve for subsequent iterations
|
|
248
|
+
const updatedFrontendDep = taskId;
|
|
249
|
+
// Patch any previously-added frontend guardrail to depend on this seedData task
|
|
250
|
+
const frontendGuard = prd.tasks.find(t => t.category === 'frontend' && t.description.includes('[GUARDRAIL]'));
|
|
251
|
+
if (frontendGuard && !frontendGuard.dependencies.includes(taskId)) {
|
|
252
|
+
frontendGuard.dependencies = [taskId];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
223
255
|
}
|
|
224
256
|
|
|
225
257
|
writeJSON('.ralph/prd.json', prd);
|
|
@@ -83,6 +83,87 @@ dotnet build --no-restore
|
|
|
83
83
|
|
|
84
84
|
MUST pass before proceeding to application/api/test/frontend. Fix if fails.
|
|
85
85
|
|
|
86
|
+
**POST-CHECK: Route kebab-case validation (BLOCKING)**
|
|
87
|
+
|
|
88
|
+
After generating NavigationSeedData files, verify routes follow kebab-case convention:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Search for routes with uppercase in NavigationSeedData files
|
|
92
|
+
UPPERCASE_ROUTES=$(grep -E 'Route = "?/[^"]*[A-Z]' Infrastructure/Persistence/Seeding/Data/*/NavigationSeedData.cs 2>/dev/null)
|
|
93
|
+
|
|
94
|
+
if [ -n "$UPPERCASE_ROUTES" ]; then
|
|
95
|
+
echo "❌ ERROR: Routes must be kebab-case lowercase. Found PascalCase in NavigationSeedData routes:"
|
|
96
|
+
echo "$UPPERCASE_ROUTES"
|
|
97
|
+
echo ""
|
|
98
|
+
echo "Expected format: /business/human-resources/projects"
|
|
99
|
+
echo "Fix: Use ToKebabCase() helper in route generation (see core-seed-data.md)"
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Why this matters:**
|
|
105
|
+
- Backend seed data defines menu navigation routes
|
|
106
|
+
- Frontend React Router expects kebab-case URLs
|
|
107
|
+
- Mismatch causes 404 on menu click
|
|
108
|
+
- Convention: `HumanResources` (C# code) → `human-resources` (web URL)
|
|
109
|
+
|
|
110
|
+
**Fix:** If check fails, routes were generated without `ToKebabCase()` transformation. Regenerate NavigationSeedData with the helper.
|
|
111
|
+
|
|
112
|
+
**POST-CHECK: Core Seed Data Integrity (BLOCKING)**
|
|
113
|
+
|
|
114
|
+
After generating ALL seed data files (NavigationApplicationSeedData, NavigationModuleSeedData, PermissionsSeedData, RolesSeedData, ApplicationRolesSeedData, IClientSeedDataProvider), verify the complete chain:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 1. NavigationApplicationSeedData.cs exists
|
|
118
|
+
APP_SEED=$(find . -path "*/Seeding/Data/NavigationApplicationSeedData.cs" 2>/dev/null | head -1)
|
|
119
|
+
if [ -z "$APP_SEED" ]; then
|
|
120
|
+
echo "❌ BLOCKING: NavigationApplicationSeedData.cs NOT FOUND"
|
|
121
|
+
echo "Without this, nav_Applications is empty → navigation menu invisible"
|
|
122
|
+
echo "Fix: Generate from core-seed-data.md section 1b using seedDataCore.navigationApplications"
|
|
123
|
+
exit 1
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# 2. ApplicationRolesSeedData.cs references NavigationApplicationSeedData.ApplicationId (no placeholder)
|
|
127
|
+
if grep -q '{ApplicationGuid}' Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs 2>/dev/null; then
|
|
128
|
+
echo "❌ BLOCKING: ApplicationRolesSeedData still has {ApplicationGuid} placeholder"
|
|
129
|
+
echo "Fix: Replace with NavigationApplicationSeedData.ApplicationId"
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# 3. IClientSeedDataProvider has no hardcoded app placeholders
|
|
134
|
+
PROVIDER=$(find . -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
135
|
+
if [ -n "$PROVIDER" ]; then
|
|
136
|
+
if grep -qE '\{appLabel_|appDesc_|appIcon\}' "$PROVIDER" 2>/dev/null; then
|
|
137
|
+
echo "❌ BLOCKING: SeedDataProvider has hardcoded {appLabel_xx}/{appIcon} placeholders"
|
|
138
|
+
echo "Fix: Use NavigationApplicationSeedData.GetApplicationEntry() and GetTranslationEntries()"
|
|
139
|
+
exit 1
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# 4. Quick startup test — verify seed data doesn't crash at runtime
|
|
144
|
+
echo "Running startup test to verify seed data..."
|
|
145
|
+
dotnet run --project "$API_PROJECT" --urls "http://localhost:0" -- --environment Development &
|
|
146
|
+
RUN_PID=$!
|
|
147
|
+
sleep 8
|
|
148
|
+
|
|
149
|
+
if ! kill -0 $RUN_PID 2>/dev/null; then
|
|
150
|
+
echo "❌ BLOCKING: Application crashed during startup (seed data likely failed)"
|
|
151
|
+
echo "Check logs for: navigation, role, or permission seed data errors"
|
|
152
|
+
wait $RUN_PID 2>/dev/null
|
|
153
|
+
exit 1
|
|
154
|
+
else
|
|
155
|
+
echo "✓ Startup test passed — seed data executed without crash"
|
|
156
|
+
kill $RUN_PID 2>/dev/null
|
|
157
|
+
wait $RUN_PID 2>/dev/null
|
|
158
|
+
fi
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Why this matters:**
|
|
162
|
+
- If `nav_Applications` is empty → navigation menu shows nothing → user can't access modules
|
|
163
|
+
- If `auth_Roles` is empty → role-permission mappings fail silently → RBAC broken
|
|
164
|
+
- If `auth_Permissions` is empty → all authorization checks reject → 403 on every endpoint
|
|
165
|
+
- These are **foundational** — without them, ALL subsequent features (frontend, API, tests) are useless
|
|
166
|
+
|
|
86
167
|
### 6. Validate with MCP
|
|
87
168
|
|
|
88
169
|
```
|