@atlashub/smartstack-cli 3.5.0 → 3.7.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 (34) hide show
  1. package/dist/index.js +13 -0
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/skills/business-analyse/SKILL.md +24 -19
  5. package/templates/skills/business-analyse/_architecture.md +1 -1
  6. package/templates/skills/business-analyse/_elicitation.md +1 -1
  7. package/templates/skills/business-analyse/_module-loop.md +4 -4
  8. package/templates/skills/business-analyse/html/ba-interactive.html +39 -10
  9. package/templates/skills/business-analyse/questionnaire/06-security.md +1 -1
  10. package/templates/skills/business-analyse/questionnaire.md +2 -2
  11. package/templates/skills/business-analyse/references/html-data-mapping.md +3 -2
  12. package/templates/skills/business-analyse/schemas/feature-schema.json +1 -1
  13. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +1 -1
  14. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +1 -0
  15. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +1 -1
  16. package/templates/skills/business-analyse/steps/step-00-init.md +37 -1
  17. package/templates/skills/business-analyse/steps/step-01-cadrage.md +166 -6
  18. package/templates/skills/business-analyse/steps/step-02-decomposition.md +4 -4
  19. package/templates/skills/business-analyse/steps/{step-03a-specify.md → step-03a-data.md} +10 -359
  20. package/templates/skills/business-analyse/steps/step-03b-ui.md +414 -0
  21. package/templates/skills/business-analyse/steps/step-03c-compile.md +343 -0
  22. package/templates/skills/business-analyse/steps/{step-03b-compile.md → step-03d-validate.md} +26 -308
  23. package/templates/skills/business-analyse/steps/step-04-consolidation.md +23 -2
  24. package/templates/skills/business-analyse/steps/step-05a-handoff.md +66 -46
  25. package/templates/skills/business-analyse/steps/step-05b-deploy.md +262 -212
  26. package/templates/skills/business-analyse/templates/tpl-frd.md +1 -1
  27. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +1 -1
  28. package/templates/skills/controller/steps/step-03-generate.md +2 -1
  29. package/templates/skills/ralph-loop/SKILL.md +20 -5
  30. package/templates/skills/ralph-loop/references/core-seed-data.md +538 -0
  31. package/templates/skills/ralph-loop/steps/step-00-init.md +79 -1
  32. package/templates/skills/ralph-loop/steps/step-01-task.md +25 -2
  33. package/templates/skills/ralph-loop/steps/step-02-execute.md +39 -15
  34. package/templates/skills/ralph-loop/steps/step-04-check.md +87 -4
@@ -0,0 +1,538 @@
1
+ # Core Seed Data - Execution Reference
2
+
3
+ > **Loaded by:** step-02-execute and step-04 compact loop
4
+ > **Condition:** Infrastructure task involves core seed data or IClientSeedDataProvider
5
+ > **Applies to:** Client projects only (seeding_strategy = "provider", ExtensionsDbContext)
6
+ >
7
+ > **Source of truth:** `/application` skill `templates-seed.md` (lines 608-916)
8
+
9
+ ---
10
+
11
+ ## 1. Parameter Extraction
12
+
13
+ Extract navigation hierarchy from the task's `_seedDataMeta` (populated by guardrail 1e):
14
+
15
+ ```javascript
16
+ const task = currentTask;
17
+ const meta = task._seedDataMeta || task._providerMeta || {};
18
+ const coreSeedData = meta.coreSeedData || {};
19
+
20
+ // Navigation hierarchy
21
+ const navModules = coreSeedData.navigationModules || coreSeedData.navigation || [];
22
+ const permissions = coreSeedData.permissions || [];
23
+ const rolePerms = coreSeedData.rolePermissions || [];
24
+
25
+ // Derived context (from guardrail or PRD)
26
+ const navRoute = meta.navRoute; // e.g. "business.humanresources.projects"
27
+ const contextCode = meta.contextCode; // e.g. "business"
28
+ const appCode = meta.appCode; // e.g. "humanresources"
29
+ const moduleCode = task.module; // e.g. "projects"
30
+
31
+ // If _seedDataMeta is absent, fallback to PRD source
32
+ if (!navRoute) {
33
+ const prd = readJSON('.ralph/prd.json');
34
+ const navRoute = `${prd.source?.context || 'business'}.${prd.source?.application || prd.metadata?.module}.${task.module}`;
35
+ }
36
+ ```
37
+
38
+ **State variables after extraction:**
39
+
40
+ | Variable | Example | Source |
41
+ |----------|---------|--------|
42
+ | `navRoute` | `business.humanresources.projects` | `_seedDataMeta.navRoute` |
43
+ | `contextCode` | `business` | `_seedDataMeta.contextCode` |
44
+ | `appCode` | `humanresources` | `_seedDataMeta.appCode` |
45
+ | `moduleCode` | `projects` | `task.module` |
46
+ | `navModules[]` | `[{code, label, icon, route, translations}]` | `coreSeedData.navigationModules` |
47
+ | `permissions[]` | `[{path, action, description}]` | `coreSeedData.permissions` |
48
+ | `rolePerms[]` | `[{role, permissions[]}]` | `coreSeedData.rolePermissions` |
49
+
50
+ ---
51
+
52
+ ## 2. NavigationModuleSeedData.cs
53
+
54
+ **File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/NavigationModuleSeedData.cs`
55
+
56
+ ### GUID Generation Rule
57
+
58
+ ```csharp
59
+ // NEVER use Guid.NewGuid() — ALWAYS deterministic
60
+ private static Guid GenerateDeterministicGuid(string seed)
61
+ {
62
+ using var sha256 = System.Security.Cryptography.SHA256.Create();
63
+ var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
64
+ return new Guid(hash.Take(16).ToArray());
65
+ }
66
+
67
+ // Usage: GUIDs are derived from the navigation path
68
+ public static readonly Guid ModuleId = GenerateDeterministicGuid("navigation-module-{navRoute}");
69
+ // Example: GenerateDeterministicGuid("navigation-module-business.humanresources.projects")
70
+ ```
71
+
72
+ ### Template
73
+
74
+ ```csharp
75
+ using SmartStack.Domain.Navigation;
76
+
77
+ namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{ModulePascal};
78
+
79
+ /// <summary>
80
+ /// Navigation seed data for {ModuleLabel} module.
81
+ /// Consumed by IClientSeedDataProvider at application startup.
82
+ /// </summary>
83
+ public static class {ModulePascal}NavigationSeedData
84
+ {
85
+ // Deterministic GUID for this module
86
+ public static readonly Guid {ModulePascal}ModuleId =
87
+ GenerateDeterministicGuid("navigation-module-{navRoute}");
88
+
89
+ /// <summary>
90
+ /// Returns navigation module entry for seeding into core.nav_Modules.
91
+ /// </summary>
92
+ public static NavigationModuleSeedEntry GetModuleEntry(Guid applicationId)
93
+ {
94
+ return new NavigationModuleSeedEntry
95
+ {
96
+ Id = {ModulePascal}ModuleId,
97
+ ApplicationId = applicationId,
98
+ Code = "{moduleCode}",
99
+ Label = "{label_en}",
100
+ Description = "{desc_en}",
101
+ Icon = "{icon}", // Lucide React icon name
102
+ IconType = IconType.Lucide,
103
+ Route = "/{contextCode}/{appCode}/{moduleCode}",
104
+ DisplayOrder = {displayOrder},
105
+ IsActive = true
106
+ };
107
+ }
108
+
109
+ /// <summary>
110
+ /// Returns 4-language translations for this module.
111
+ /// </summary>
112
+ public static IEnumerable<NavigationTranslationSeedEntry> GetTranslationEntries()
113
+ {
114
+ var moduleId = {ModulePascal}ModuleId;
115
+ return new[]
116
+ {
117
+ new NavigationTranslationSeedEntry
118
+ {
119
+ EntityType = NavigationEntityType.Module,
120
+ EntityId = moduleId,
121
+ LanguageCode = "fr",
122
+ Label = "{label_fr}",
123
+ Description = "{desc_fr}"
124
+ },
125
+ new NavigationTranslationSeedEntry
126
+ {
127
+ EntityType = NavigationEntityType.Module,
128
+ EntityId = moduleId,
129
+ LanguageCode = "en",
130
+ Label = "{label_en}",
131
+ Description = "{desc_en}"
132
+ },
133
+ new NavigationTranslationSeedEntry
134
+ {
135
+ EntityType = NavigationEntityType.Module,
136
+ EntityId = moduleId,
137
+ LanguageCode = "it",
138
+ Label = "{label_it}",
139
+ Description = "{desc_it}"
140
+ },
141
+ new NavigationTranslationSeedEntry
142
+ {
143
+ EntityType = NavigationEntityType.Module,
144
+ EntityId = moduleId,
145
+ LanguageCode = "de",
146
+ Label = "{label_de}",
147
+ Description = "{desc_de}"
148
+ }
149
+ };
150
+ }
151
+
152
+ private static Guid GenerateDeterministicGuid(string seed)
153
+ {
154
+ using var sha256 = System.Security.Cryptography.SHA256.Create();
155
+ var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
156
+ return new Guid(hash.Take(16).ToArray());
157
+ }
158
+ }
159
+
160
+ /// <summary>Seed entry DTO for navigation module.</summary>
161
+ public class NavigationModuleSeedEntry
162
+ {
163
+ public Guid Id { get; init; }
164
+ public Guid ApplicationId { get; init; }
165
+ public string Code { get; init; } = null!;
166
+ public string Label { get; init; } = null!;
167
+ public string Description { get; init; } = null!;
168
+ public string Icon { get; init; } = null!;
169
+ public IconType IconType { get; init; }
170
+ public string Route { get; init; } = null!;
171
+ public int DisplayOrder { get; init; }
172
+ public bool IsActive { get; init; }
173
+ }
174
+
175
+ /// <summary>Seed entry DTO for navigation translation.</summary>
176
+ public class NavigationTranslationSeedEntry
177
+ {
178
+ public NavigationEntityType EntityType { get; init; }
179
+ public Guid EntityId { get; init; }
180
+ public string LanguageCode { get; init; } = null!;
181
+ public string Label { get; init; } = null!;
182
+ public string Description { get; init; } = null!;
183
+ }
184
+ ```
185
+
186
+ **Replace placeholders** with values from `navModules[]` and PRD `project` metadata.
187
+
188
+ ---
189
+
190
+ ## 3. PermissionsSeedData.cs — MCP-First
191
+
192
+ ### Step A: Call MCP (PRIMARY)
193
+
194
+ ```
195
+ Tool: mcp__smartstack__generate_permissions
196
+ Args:
197
+ navRoute: "{navRoute}"
198
+ includeStandardActions: true
199
+ includeWildcard: true
200
+ ```
201
+
202
+ MCP returns:
203
+ - `Permissions.cs` nested class (Application layer constants)
204
+ - Permission seed entries with deterministic GUIDs
205
+
206
+ ### Step B: Write Permissions.cs (Application layer)
207
+
208
+ ```csharp
209
+ // Add to Application/Common/Authorization/Permissions.cs
210
+ public static class {ContextPascal}
211
+ {
212
+ public const string Access = "{contextCode}.{appCode}";
213
+
214
+ public static class {ModulePascal}
215
+ {
216
+ public const string View = "{navRoute}.read";
217
+ public const string Create = "{navRoute}.create";
218
+ public const string Update = "{navRoute}.update";
219
+ public const string Delete = "{navRoute}.delete";
220
+ }
221
+ }
222
+ ```
223
+
224
+ ### Step C: Write PermissionsSeedData.cs (Infrastructure layer)
225
+
226
+ **File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/PermissionsSeedData.cs`
227
+
228
+ ```csharp
229
+ namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{ModulePascal};
230
+
231
+ /// <summary>
232
+ /// Permission seed data for {ModuleLabel} module.
233
+ /// Consumed by IClientSeedDataProvider at application startup.
234
+ /// </summary>
235
+ public static class {ModulePascal}PermissionsSeedData
236
+ {
237
+ // Deterministic GUIDs for permissions
238
+ public static readonly Guid WildcardPermId = GenerateGuid("{navRoute}.*");
239
+ public static readonly Guid ReadPermId = GenerateGuid("{navRoute}.read");
240
+ public static readonly Guid CreatePermId = GenerateGuid("{navRoute}.create");
241
+ public static readonly Guid UpdatePermId = GenerateGuid("{navRoute}.update");
242
+ public static readonly Guid DeletePermId = GenerateGuid("{navRoute}.delete");
243
+
244
+ public static IEnumerable<PermissionSeedEntry> GetPermissionEntries(Guid moduleId)
245
+ {
246
+ return new[]
247
+ {
248
+ new PermissionSeedEntry { Id = WildcardPermId, Path = "{navRoute}.*", Level = PermissionLevel.Module, Action = PermissionAction.Access, IsWildcard = true, ModuleId = moduleId, Description = "Full {moduleLabel} access" },
249
+ new PermissionSeedEntry { Id = ReadPermId, Path = "{navRoute}.read", Level = PermissionLevel.Module, Action = PermissionAction.Read, IsWildcard = false, ModuleId = moduleId, Description = "View {moduleLabel}" },
250
+ new PermissionSeedEntry { Id = CreatePermId, Path = "{navRoute}.create", Level = PermissionLevel.Module, Action = PermissionAction.Create, IsWildcard = false, ModuleId = moduleId, Description = "Create {moduleLabel}" },
251
+ new PermissionSeedEntry { Id = UpdatePermId, Path = "{navRoute}.update", Level = PermissionLevel.Module, Action = PermissionAction.Update, IsWildcard = false, ModuleId = moduleId, Description = "Update {moduleLabel}" },
252
+ new PermissionSeedEntry { Id = DeletePermId, Path = "{navRoute}.delete", Level = PermissionLevel.Module, Action = PermissionAction.Delete, IsWildcard = false, ModuleId = moduleId, Description = "Delete {moduleLabel}" }
253
+ };
254
+ }
255
+
256
+ private static Guid GenerateGuid(string path)
257
+ {
258
+ using var sha256 = System.Security.Cryptography.SHA256.Create();
259
+ var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"permission-{path}"));
260
+ return new Guid(hash.Take(16).ToArray());
261
+ }
262
+ }
263
+
264
+ public class PermissionSeedEntry
265
+ {
266
+ public Guid Id { get; init; }
267
+ public string Path { get; init; } = null!;
268
+ public PermissionLevel Level { get; init; }
269
+ public PermissionAction Action { get; init; }
270
+ public bool IsWildcard { get; init; }
271
+ public Guid ModuleId { get; init; }
272
+ public string Description { get; init; } = null!;
273
+ }
274
+ ```
275
+
276
+ ### Step D: MCP Fallback
277
+
278
+ If MCP `generate_permissions` fails, use the template above directly with values derived from the PRD `coreSeedData.permissions[]`.
279
+
280
+ ---
281
+
282
+ ## 4. RolesSeedData.cs
283
+
284
+ **File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/RolesSeedData.cs`
285
+
286
+ ### Context-Based Role Mapping
287
+
288
+ | Context | Admin | Manager | Contributor | Viewer |
289
+ |---------|-------|---------|-------------|--------|
290
+ | `platform.*` | CRUD | CRU | CR | R |
291
+ | `business.*` | CRUD | CRU | CR | R |
292
+ | `personal.*` | CRUD | CRU | CR | R |
293
+
294
+ ### Template
295
+
296
+ ```csharp
297
+ namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{ModulePascal};
298
+
299
+ /// <summary>
300
+ /// Role-permission mapping seed data for {ModuleLabel} module.
301
+ /// Maps permissions to application-scoped roles (Admin, Manager, Contributor, Viewer).
302
+ /// Consumed by IClientSeedDataProvider at application startup.
303
+ /// </summary>
304
+ public static class {ModulePascal}RolesSeedData
305
+ {
306
+ /// <summary>
307
+ /// Returns role-permission mappings for this module.
308
+ /// Roles are resolved at runtime by Code (not hardcoded GUIDs).
309
+ /// </summary>
310
+ public static IEnumerable<RolePermissionSeedEntry> GetRolePermissionEntries()
311
+ {
312
+ // Admin: wildcard access
313
+ yield return new RolePermissionSeedEntry { RoleCode = "admin", PermissionPath = "{navRoute}.*" };
314
+
315
+ // Manager: CRUD
316
+ yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.read" };
317
+ yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.create" };
318
+ yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.update" };
319
+
320
+ // Contributor: CR
321
+ yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.read" };
322
+ yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.create" };
323
+
324
+ // Viewer: R
325
+ yield return new RolePermissionSeedEntry { RoleCode = "viewer", PermissionPath = "{navRoute}.read" };
326
+ }
327
+ }
328
+
329
+ public class RolePermissionSeedEntry
330
+ {
331
+ public string RoleCode { get; init; } = null!;
332
+ public string PermissionPath { get; init; } = null!;
333
+ }
334
+ ```
335
+
336
+ ---
337
+
338
+ ## 5. IClientSeedDataProvider Implementation
339
+
340
+ **File:** `Infrastructure/Persistence/Seeding/{AppPascalName}SeedDataProvider.cs`
341
+
342
+ ### Critical Rules
343
+
344
+ | Rule | Description |
345
+ |------|-------------|
346
+ | Factory methods | `NavigationModule.Create(...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()` |
347
+ | Idempotence | Each Seed method checks existence before inserting |
348
+ | SaveChanges per group | Navigation -> save -> Permissions -> save -> RolePermissions -> save |
349
+ | FK resolution by Code | Parent entities found by `Code`, not hardcoded GUID |
350
+ | DI registration | `services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()` |
351
+
352
+ ### Template
353
+
354
+ ```csharp
355
+ using Microsoft.EntityFrameworkCore;
356
+ using SmartStack.Application.Common.Interfaces;
357
+ using SmartStack.Application.Common.Interfaces.Seeding;
358
+ using SmartStack.Domain.Navigation;
359
+ using SmartStack.Domain.Platform.Administration.Roles;
360
+ using {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{Module1Pascal};
361
+ // using {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{Module2Pascal}; // Add per module
362
+
363
+ namespace {BaseNamespace}.Infrastructure.Persistence.Seeding;
364
+
365
+ /// <summary>
366
+ /// Seeds {AppLabel} navigation, permissions, and role-permission data
367
+ /// into the SmartStack Core schema at application startup.
368
+ /// </summary>
369
+ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
370
+ {
371
+ public int Order => 100;
372
+
373
+ public async Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct)
374
+ {
375
+ // --- Application ---
376
+ var exists = await context.NavigationApplications
377
+ .AnyAsync(a => a.Code == "{appCode}", ct);
378
+ if (exists) return;
379
+
380
+ var parentContext = await context.NavigationContexts
381
+ .FirstAsync(c => c.Code == "{contextCode}", ct);
382
+
383
+ var app = NavigationApplication.Create(
384
+ parentContext.Id, "{appCode}", "{appLabel_en}",
385
+ "{appDesc_en}", "{appIcon}", IconType.Lucide,
386
+ "/{contextCode}/{appCode}", 1);
387
+ context.NavigationApplications.Add(app);
388
+ await ((DbContext)context).SaveChangesAsync(ct);
389
+
390
+ // --- Application translations (4 languages) ---
391
+ var appTranslations = new[]
392
+ {
393
+ NavigationTranslation.Create(NavigationEntityType.Application, app.Id, "fr", "{appLabel_fr}", "{appDesc_fr}"),
394
+ NavigationTranslation.Create(NavigationEntityType.Application, app.Id, "en", "{appLabel_en}", "{appDesc_en}"),
395
+ NavigationTranslation.Create(NavigationEntityType.Application, app.Id, "it", "{appLabel_it}", "{appDesc_it}"),
396
+ NavigationTranslation.Create(NavigationEntityType.Application, app.Id, "de", "{appLabel_de}", "{appDesc_de}")
397
+ };
398
+ foreach (var t in appTranslations) context.NavigationTranslations.Add(t);
399
+ await ((DbContext)context).SaveChangesAsync(ct);
400
+
401
+ // --- Modules ---
402
+ // Module: {Module1}
403
+ var mod1Entry = {Module1Pascal}NavigationSeedData.GetModuleEntry(app.Id);
404
+ var mod1 = NavigationModule.Create(
405
+ mod1Entry.ApplicationId, mod1Entry.Code, mod1Entry.Label,
406
+ mod1Entry.Description, mod1Entry.Icon, mod1Entry.IconType,
407
+ mod1Entry.Route, mod1Entry.DisplayOrder);
408
+ context.NavigationModules.Add(mod1);
409
+
410
+ // Repeat for each module...
411
+ await ((DbContext)context).SaveChangesAsync(ct);
412
+
413
+ // --- Module translations ---
414
+ foreach (var t in {Module1Pascal}NavigationSeedData.GetTranslationEntries())
415
+ {
416
+ context.NavigationTranslations.Add(
417
+ NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
418
+ }
419
+ // Repeat for each module...
420
+ await ((DbContext)context).SaveChangesAsync(ct);
421
+ }
422
+
423
+ public async Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct)
424
+ {
425
+ var exists = await context.Permissions
426
+ .AnyAsync(p => p.Path == "{contextCode}.{appCode}.*", ct);
427
+ if (exists) return;
428
+
429
+ // Application-level wildcard
430
+ var appWildcard = Permission.CreateWildcard(
431
+ "{contextCode}.{appCode}.*", PermissionLevel.Application,
432
+ "Full {appLabel_en} access");
433
+ context.Permissions.Add(appWildcard);
434
+
435
+ // Module permissions
436
+ var mod1 = await context.NavigationModules
437
+ .FirstAsync(m => m.Code == "{module1Code}", ct);
438
+ foreach (var entry in {Module1Pascal}PermissionsSeedData.GetPermissionEntries(mod1.Id))
439
+ {
440
+ var perm = entry.IsWildcard
441
+ ? Permission.CreateWildcard(entry.Path, entry.Level, entry.Description)
442
+ : Permission.CreateForModule(entry.Path, entry.Level, entry.Action, false, entry.ModuleId, entry.Description);
443
+ context.Permissions.Add(perm);
444
+ }
445
+
446
+ // Repeat for each module...
447
+ await ((DbContext)context).SaveChangesAsync(ct);
448
+ }
449
+
450
+ public async Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct)
451
+ {
452
+ var exists = await context.RolePermissions
453
+ .AnyAsync(rp => rp.Permission!.Path.StartsWith("{contextCode}.{appCode}."), ct);
454
+ if (exists) return;
455
+
456
+ // Resolve roles by Code
457
+ var roles = await context.Roles
458
+ .Where(r => r.ApplicationId != null || r.IsSystem)
459
+ .ToListAsync(ct);
460
+
461
+ // Resolve permissions
462
+ var permissions = await context.Permissions
463
+ .Where(p => p.Path.StartsWith("{contextCode}.{appCode}."))
464
+ .ToListAsync(ct);
465
+
466
+ // Apply role-permission mappings from all modules
467
+ var allMappings = new List<RolePermissionSeedEntry>();
468
+ allMappings.AddRange({Module1Pascal}RolesSeedData.GetRolePermissionEntries());
469
+ // allMappings.AddRange({Module2Pascal}RolesSeedData.GetRolePermissionEntries()); // per module
470
+
471
+ foreach (var mapping in allMappings)
472
+ {
473
+ var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);
474
+ var perm = permissions.FirstOrDefault(p => p.Path == mapping.PermissionPath);
475
+ if (role != null && perm != null)
476
+ {
477
+ context.RolePermissions.Add(RolePermission.Create(role.Id, perm.Id, "system"));
478
+ }
479
+ }
480
+
481
+ await ((DbContext)context).SaveChangesAsync(ct);
482
+ }
483
+ }
484
+ ```
485
+
486
+ ### DI Registration
487
+
488
+ ```csharp
489
+ // In Infrastructure/DependencyInjection.cs — add:
490
+ using SmartStack.Application.Common.Interfaces.Seeding;
491
+
492
+ // In the registration method:
493
+ services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>();
494
+ ```
495
+
496
+ ---
497
+
498
+ ## 6. Multi-Module Handling
499
+
500
+ When processing multiple modules in the same ralph-loop run:
501
+
502
+ ### Module 1 (first): Creates everything from scratch
503
+
504
+ 1. `{Module1}NavigationSeedData.cs`
505
+ 2. `{Module1}PermissionsSeedData.cs`
506
+ 3. `{Module1}RolesSeedData.cs`
507
+ 4. `{AppPascalName}SeedDataProvider.cs` (new)
508
+ 5. DI registration (new)
509
+
510
+ ### Module 2+ (subsequent): Append to existing provider
511
+
512
+ 1. `{Module2}NavigationSeedData.cs` (new file)
513
+ 2. `{Module2}PermissionsSeedData.cs` (new file)
514
+ 3. `{Module2}RolesSeedData.cs` (new file)
515
+ 4. `{AppPascalName}SeedDataProvider.cs` (**modify** — add using, add entries in each Seed method)
516
+ 5. DI registration (already exists — skip)
517
+
518
+ **Detection:** Check if `{AppPascalName}SeedDataProvider.cs` exists. If yes, READ it and ADD the new module's entries to each of the 3 methods.
519
+
520
+ ---
521
+
522
+ ## 7. Verification Checklist (BLOCKING)
523
+
524
+ Before marking the task as completed, verify ALL:
525
+
526
+ - [ ] Deterministic GUIDs (NEVER `Guid.NewGuid()`) — SHA256 of path
527
+ - [ ] 4 languages for each navigation entity (fr, en, it, de)
528
+ - [ ] `Permissions.cs` constants match seed data paths
529
+ - [ ] MCP `generate_permissions` called (or fallback used)
530
+ - [ ] Role-permission mappings assigned (Admin, Manager, Contributor, Viewer)
531
+ - [ ] `IClientSeedDataProvider` generated with 3 methods
532
+ - [ ] Each Seed method is idempotent (checks existence before inserting)
533
+ - [ ] Factory methods used throughout (NEVER `new Entity()`)
534
+ - [ ] `SaveChangesAsync` called per group (Navigation → Permissions → RolePermissions)
535
+ - [ ] DI registration added: `services.AddScoped<IClientSeedDataProvider, ...>()`
536
+ - [ ] `dotnet build` passes after generation
537
+
538
+ **If ANY check fails, the task status = 'failed'.**
@@ -14,6 +14,8 @@ next_step: steps/step-01-task.md
14
14
  - YOU ARE AN INITIALIZER, not an executor
15
15
  - FORBIDDEN to load step-01 until init is complete
16
16
  - ALWAYS check prd.json version is v2
17
+ - **CONTEXT BUDGET**: Keep init output COMPACT. Do NOT dump verbose MCP responses or long file listings. Every token saved here = more tokens for actual code generation in the COMPACT LOOP.
18
+ - **NEVER DELEGATE**: Do NOT use the Task tool to delegate the Ralph loop to a sub-agent. The loop MUST run in the main agent context. Sub-agents lose skill context and stop prematurely.
17
19
 
18
20
  ## YOUR TASK:
19
21
 
@@ -199,7 +201,73 @@ mkdir -p .ralph/logs
199
201
  mkdir -p .ralph/reports
200
202
  ```
201
203
 
202
- ### 4b. Detect Multi-Module PRDs (from BA Handoff)
204
+ ### 4a. Auto-Recovery: Detect BA Artifacts Without PRD (NEW)
205
+
206
+ > **Scenario:** Business-analyse completed (feature.json with status="handed-off") but step-05b
207
+ > didn't generate PRD files (context exhaustion). Ralph-loop auto-recovers by running `ss derive-prd`.
208
+
209
+ **Check for BA artifacts when no PRD files exist:**
210
+
211
+ ```bash
212
+ PRD_EXISTS=$(ls .ralph/prd-*.json 2>/dev/null | head -1)
213
+ SINGLE_PRD_EXISTS=$(test -f .ralph/prd.json && echo "yes" || echo "no")
214
+ ```
215
+
216
+ **IF no PRD files found (neither prd-*.json nor prd.json):**
217
+
218
+ ```bash
219
+ # Search for handed-off master feature.json
220
+ MASTER_FEATURE=$(find docs/business -maxdepth 4 -name "feature.json" -path "*/business-analyse/*" 2>/dev/null | head -1)
221
+ ```
222
+
223
+ **IF master feature.json found:**
224
+
225
+ ```javascript
226
+ const master = readJSON(MASTER_FEATURE);
227
+
228
+ if (master.status === "handed-off" || master.handoff?.status === "handed-off") {
229
+ // BA completed but PRD files are missing — auto-recover
230
+ console.log("BA artifacts detected without PRD files — auto-recovering...");
231
+
232
+ // Display validation table
233
+ console.log(`
234
+ ╔══════════════════════════════════════════════════════════════════╗
235
+ ║ BA ARTIFACT VALIDATION ║
236
+ ╠══════════════════════════════════════════════════════════════════╣
237
+ ║ Master feature.json: ✅ Found (status: ${master.status}) ║
238
+ ║ Application: ${master.metadata?.application || 'unknown'} ║
239
+ ║ Modules: ${master.modules?.length || 0} ║
240
+ ║ PRD files: ❌ Missing — will auto-generate ║
241
+ ╠══════════════════════════════════════════════════════════════════╣
242
+ ║ Running: ss derive-prd --application ${MASTER_FEATURE} ║
243
+ ╚══════════════════════════════════════════════════════════════════╝
244
+ `);
245
+
246
+ // Execute ss derive-prd to generate missing PRD files
247
+ // This is a deterministic CLI command (no LLM), safe to auto-run
248
+ exec(`ss derive-prd --application ${MASTER_FEATURE}`);
249
+
250
+ // Verify PRD files were generated
251
+ const newPrdFiles = glob('.ralph/prd-*.json');
252
+ if (newPrdFiles.length === 0) {
253
+ console.log("❌ ss derive-prd failed to generate PRD files");
254
+ console.log(" Verify: ss derive-prd is installed (npm list -g @atlashub/smartstack-cli)");
255
+ console.log(" Manual: ss derive-prd --application " + MASTER_FEATURE);
256
+ STOP;
257
+ }
258
+
259
+ console.log(`✅ Auto-recovered ${newPrdFiles.length} PRD files`);
260
+ // Continue to section 4b which will detect and queue them
261
+ }
262
+ ```
263
+
264
+ **IF master feature.json NOT found OR status ≠ "handed-off":**
265
+ - No BA artifacts → proceed normally (step-01 will create prd.json from task description)
266
+
267
+ **IF PRD files already exist:**
268
+ - Skip auto-recovery entirely → proceed to section 4b
269
+
270
+ ### 4b. Detect Multi-Module PRDs (from BA Handoff or Auto-Recovery)
203
271
 
204
272
  **After creating `.ralph/` directory, check for per-module PRD files:**
205
273
 
@@ -263,6 +331,16 @@ Queue: {module codes joined by " → "}
263
331
  Starting with: {modules[0].code}
264
332
  ```
265
333
 
334
+ 6. **Display BA artifact validation (if from BA handoff):**
335
+ ```
336
+ [CHECK] BA Artifact Validation:
337
+ ✓ Master feature.json: {MASTER_FEATURE} (status: handed-off)
338
+ ✓ Module feature.json: {module_count} modules (all handed-off)
339
+ ✓ PRD files: {PRD_COUNT} modules (.ralph/prd-*.json)
340
+ ✓ progress.txt: {exists ? "present" : "will be created"}
341
+ [PASS] All BA artifacts valid. Multi-module development ready.
342
+ ```
343
+
266
344
  **If only `.ralph/prd.json` exists (single module):**
267
345
  - Skip queue creation (backward compatible)
268
346
  - Proceed normally
@@ -499,7 +499,19 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
499
499
  "src/Infrastructure/Persistence/Seeding/Data/SeedConstants.cs"
500
500
  ], modified: [] },
501
501
  validation: null, error: null, module: moduleCode,
502
- _seedDataMeta: { source: "guardrail-derived", coreSeedData }
502
+ _seedDataMeta: {
503
+ source: "guardrail-derived",
504
+ coreSeedData,
505
+ navRoute: prdJson.project?.navRoute
506
+ || (permissions?.[0]?.path?.split('.').slice(0, -1).join('.'))
507
+ || `business.${prdJson.project?.application || 'app'}.${moduleCode}`,
508
+ contextCode: prdJson.project?.context
509
+ || coreSeedData.navigationModules?.[0]?.route?.split('/')?.[1]
510
+ || 'business',
511
+ appCode: prdJson.project?.application
512
+ || coreSeedData.navigationModules?.[0]?.route?.split('/')?.[2],
513
+ appLabels: prdJson.project?.labels || null
514
+ }
503
515
  });
504
516
  lastIdByCategory["infrastructure"] = taskId;
505
517
  taskId++;
@@ -530,7 +542,18 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
530
542
  ].join("; "),
531
543
  started_at: null, completed_at: null, iteration: null, commit_hash: null,
532
544
  files_changed: { created: ["src/Infrastructure/Persistence/Seeding/{AppPascalName}SeedDataProvider.cs"], modified: ["src/Infrastructure/DependencyInjection.cs"] },
533
- validation: null, error: null, module: moduleCode
545
+ validation: null, error: null, module: moduleCode,
546
+ _providerMeta: {
547
+ consumesSeedDataFiles: [
548
+ `Infrastructure/Persistence/Seeding/Data/${moduleCode}/NavigationModuleSeedData.cs`,
549
+ `Infrastructure/Persistence/Seeding/Data/${moduleCode}/PermissionsSeedData.cs`,
550
+ `Infrastructure/Persistence/Seeding/Data/${moduleCode}/RolesSeedData.cs`
551
+ ],
552
+ navRoute: prdJson.project?.navRoute
553
+ || `business.${prdJson.project?.application || 'app'}.${moduleCode}`,
554
+ contextCode: prdJson.project?.context || 'business',
555
+ appCode: prdJson.project?.application
556
+ }
534
557
  });
535
558
  lastIdByCategory["infrastructure"] = taskId;
536
559
  taskId++;