@atlashub/smartstack-cli 3.4.1 → 3.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -21,7 +21,7 @@ Domain → Application → Infrastructure → API → Web
21
21
  |-------|-----------------|-----------|------------------|
22
22
  | Domain | Entities, ValueObjects | `SmartStack.Domain.{Context}.{App}.{Module}` | `Domain/{Context}/{App}/{Module}/` |
23
23
  | Application | Interfaces (Common), DTOs | `SmartStack.Application.Common.Interfaces` / `.{Context}.{App}.{Module}.DTOs` | `Application/Common/Interfaces/` + `Application/{Context}/{App}/{Module}/DTOs/` |
24
- | Infrastructure | Service Impl, EF Core | `SmartStack.Infrastructure.Services.{Module}` | `Infrastructure/Services/{Module}/` + `Persistence/Configurations/{Module}/` |
24
+ | Infrastructure | Service Impl, EF Core | `SmartStack.Infrastructure.Services.{Context}.{App}.{Module}` | `Infrastructure/Services/{Context}/{App}/{Module}/` + `Persistence/Configurations/{Context}/{App}/{Module}/` |
25
25
  | API | Controllers, Middleware | `SmartStack.Api.Controllers.{Context}` | `Api/Controllers/{Context}/` |
26
26
  | Web | React components, pages | N/A | `pages/{context}/{app}/{module}/` + `components/{context}/{module}/` |
27
27
 
@@ -86,9 +86,9 @@ Args:
86
86
 
87
87
  The tool generates (paths organized by navRoute hierarchy `{context}.{application}.{module}`):
88
88
  - `Domain/{Context}/{Application}/{Module}/{EntityName}.cs` - Entity with BaseEntity
89
- - `Infrastructure/Persistence/Configurations/{Module}/{EntityName}Configuration.cs` - EF Config
89
+ - `Infrastructure/Persistence/Configurations/{Context}/{Application}/{Module}/{EntityName}Configuration.cs` - EF Config
90
90
  - `Application/Common/Interfaces/I{EntityName}Service.cs` - Service Interface
91
- - `Infrastructure/Services/{Module}/{EntityName}Service.cs` - Service Implementation
91
+ - `Infrastructure/Services/{Context}/{Application}/{Module}/{EntityName}Service.cs` - Service Implementation
92
92
  - `Api/Controllers/{controller_folder}/{Application}/{EntityName}Controller.cs` - REST Controller
93
93
  - `Application/{Context}/{Application}/{Module}/DTOs/{EntityName}ResponseDto.cs` - Response DTO
94
94
  - `Application/{Context}/{Application}/{Module}/DTOs/Create{EntityName}Dto.cs` - Create request
@@ -103,8 +103,8 @@ The tool generates (paths organized by navRoute hierarchy `{context}.{applicatio
103
103
  - `Domain/{Context}/{Application}/{Module}/{EntityName}.cs`
104
104
 
105
105
  ### Infrastructure Layer
106
- - `Persistence/Configurations/{Module}/{EntityName}Configuration.cs`
107
- - `Services/{Module}/{EntityName}Service.cs`
106
+ - `Persistence/Configurations/{Context}/{Application}/{Module}/{EntityName}Configuration.cs`
107
+ - `Services/{Context}/{Application}/{Module}/{EntityName}Service.cs`
108
108
 
109
109
  ### Application Layer
110
110
  - `Common/Interfaces/I{EntityName}Service.cs`
@@ -83,7 +83,7 @@ public interface I$MODULE_PASCALService
83
83
  ## TEMPLATE: SERVICE IMPLEMENTATION
84
84
 
85
85
  ```csharp
86
- // src/SmartStack.Infrastructure/Services/$MODULE_PASCAL/$MODULE_PASCALService.cs
86
+ // src/SmartStack.Infrastructure/Services/$CONTEXT_PASCAL/$APPLICATION_PASCAL/$MODULE_PASCAL/$MODULE_PASCALService.cs
87
87
 
88
88
  using Microsoft.EntityFrameworkCore;
89
89
  using Microsoft.Extensions.Logging;
@@ -91,7 +91,7 @@ using SmartStack.Application.Common.Interfaces;
91
91
  using SmartStack.Application.$CONTEXT_PASCAL.$APPLICATION_PASCAL.$MODULE_PASCAL.DTOs;
92
92
  using SmartStack.Domain.$CONTEXT_PASCAL.$APPLICATION_PASCAL.$MODULE_PASCAL;
93
93
 
94
- namespace SmartStack.Infrastructure.Services.$MODULE_PASCAL;
94
+ namespace SmartStack.Infrastructure.Services.$CONTEXT_PASCAL.$APPLICATION_PASCAL.$MODULE_PASCAL;
95
95
 
96
96
  public class $MODULE_PASCALService : I$MODULE_PASCALService
97
97
  {
@@ -359,13 +359,13 @@ public class $ENTITY_PASCAL : BaseEntity
359
359
  ## TEMPLATE: EF CONFIGURATION
360
360
 
361
361
  ```csharp
362
- // src/SmartStack.Infrastructure/Persistence/Configurations/$MODULE_PASCAL/$ENTITY_PASCALConfiguration.cs
362
+ // src/SmartStack.Infrastructure/Persistence/Configurations/$CONTEXT_PASCAL/$APPLICATION_PASCAL/$MODULE_PASCAL/$ENTITY_PASCALConfiguration.cs
363
363
 
364
364
  using Microsoft.EntityFrameworkCore;
365
365
  using Microsoft.EntityFrameworkCore.Metadata.Builders;
366
366
  using SmartStack.Domain.$CONTEXT_PASCAL.$APPLICATION_PASCAL.$MODULE_PASCAL;
367
367
 
368
- namespace SmartStack.Infrastructure.Persistence.Configurations.$MODULE_PASCAL;
368
+ namespace SmartStack.Infrastructure.Persistence.Configurations.$CONTEXT_PASCAL.$APPLICATION_PASCAL.$MODULE_PASCAL;
369
369
 
370
370
  public class $ENTITY_PASCALConfiguration : IEntityTypeConfiguration<$ENTITY_PASCAL>
371
371
  {
@@ -63,9 +63,9 @@
63
63
  | **Domain** | `{Project}.Domain/Business/{Application}/{Module}/` | `Domain/Business/Operations/FreeBicke/FreeBicke.cs` |
64
64
  | **Application/DTOs** | `{Project}.Application/Business/{Application}/{Module}/DTOs/` | `Application/Business/Operations/FreeBicke/DTOs/FreeBickeDto.cs` |
65
65
  | **Application/Interfaces** | `{Project}.Application/Common/Interfaces/` | `Application/Common/Interfaces/IFreeBickeService.cs` |
66
- | **Infrastructure/Config** | `{Project}.Infrastructure/Persistence/Configurations/{Module}/` | `Configurations/FreeBicke/FreeBickeConfiguration.cs` |
67
- | **Infrastructure/Services** | `{Project}.Infrastructure/Services/{Module}/` | `Services/FreeBicke/FreeBickeService.cs` |
68
- | **Infrastructure/SeedData** | `{Project}.Infrastructure/Persistence/Seeding/Data/{Module}/` | `Seeding/Data/FreeBicke/FreeBickeSeedData.cs` |
66
+ | **Infrastructure/Config** | `{Project}.Infrastructure/Persistence/Configurations/{Context}/{Application}/{Module}/` | `Configurations/Business/Operations/FreeBicke/FreeBickeConfiguration.cs` |
67
+ | **Infrastructure/Services** | `{Project}.Infrastructure/Services/{Context}/{Application}/{Module}/` | `Services/Business/Operations/FreeBicke/FreeBickeService.cs` |
68
+ | **Infrastructure/SeedData** | `{Project}.Infrastructure/Persistence/Seeding/Data/{Context}/{Application}/{Module}/` | `Seeding/Data/Business/Operations/FreeBicke/FreeBickeSeedData.cs` |
69
69
  | **API/Controllers** | `{Project}.Api/Controllers/Business/{Application}/` | `Controllers/Business/Operations/FreeBickeController.cs` |
70
70
  | **Frontend/Pages** | `web/src/pages/business/{application}/{module}/` | `pages/business/operations/freebicke/page.tsx` |
71
71
  | **Frontend/i18n** | `web/src/i18n/locales/{lang}/{module}.json` | `locales/fr/freebicke.json` |
@@ -78,6 +78,49 @@ prd.json generated for module {moduleCode}:
78
78
  - prd.json version: "2.0.0"
79
79
  - source.type: "ba-handoff-programmatic"
80
80
 
81
+ **POST-CHECK (BLOCKING — DO NOT SKIP):**
82
+
83
+ After `ss derive-prd` execution, read the generated prd-{moduleCode}.json and verify structural integrity:
84
+
85
+ ```javascript
86
+ const prd = readJSON(`.ralph/prd-${moduleCode}.json`);
87
+ const featureHandoff = moduleFeature.handoff.filesToCreate;
88
+
89
+ // 1. Verify structure: filesToCreate MUST be under implementation (NOT root level)
90
+ if (prd.filesToCreate && !prd.implementation?.filesToCreate) {
91
+ BLOCKING_ERROR("prd.json has filesToCreate at ROOT level — this is NOT from ss derive-prd");
92
+ BLOCKING_ERROR("Re-run: ss derive-prd --feature {path} --output .ralph/prd-{moduleCode}.json");
93
+ STOP;
94
+ }
95
+
96
+ // 2. Verify ALL 7 categories present and match feature.json
97
+ const categories = ['domain', 'application', 'infrastructure', 'api', 'frontend', 'seedData', 'tests'];
98
+ for (const cat of categories) {
99
+ const prdCount = prd.implementation.filesToCreate[cat]?.length ?? 0;
100
+ const featureCount = featureHandoff[cat]?.length ?? 0;
101
+ if (prdCount !== featureCount) {
102
+ BLOCKING_ERROR(`${cat}: prd has ${prdCount} files but feature.json has ${featureCount}`);
103
+ }
104
+ }
105
+ ```
106
+
107
+ Display verification table:
108
+
109
+ ```
110
+ POST-CHECK: prd-{moduleCode}.json integrity
111
+ | Category | feature.json | prd.json | Match |
112
+ |----------------|-------------|----------|-------|
113
+ | domain | {n} | {n} | OK/FAIL |
114
+ | application | {n} | {n} | OK/FAIL |
115
+ | infrastructure | {n} | {n} | OK/FAIL |
116
+ | api | {n} | {n} | OK/FAIL |
117
+ | frontend | {n} | {n} | OK/FAIL |
118
+ | seedData | {n} | {n} | OK/FAIL |
119
+ | tests | {n} | {n} | OK/FAIL |
120
+ ```
121
+
122
+ IF ANY category shows FAIL → **STOP AND RE-RUN `ss derive-prd`**. DO NOT proceed with incomplete PRD.
123
+
81
124
  ---
82
125
 
83
126
  ### 2. Initialize Progress Tracker
@@ -47,8 +47,8 @@ BACKEND:
47
47
  - src/{project_namespace}.Domain/Business/ -> Entity structure + folder hierarchy
48
48
  - src/{project_namespace}.Application/Business/ -> DTOs + service interfaces
49
49
  - src/{project_namespace}.Application/Common/Interfaces/ -> Service contracts
50
- - src/{project_namespace}.Infrastructure/Persistence/Configurations/ -> EF Core configs (subfolder per module)
51
- - src/{project_namespace}.Infrastructure/Services/ -> Service implementations (subfolder per module)
50
+ - src/{project_namespace}.Infrastructure/Persistence/Configurations/ -> EF Core configs (subfolder per context/app/module)
51
+ - src/{project_namespace}.Infrastructure/Services/ -> Service implementations (subfolder per context/app/module)
52
52
  - src/{project_namespace}.Infrastructure/Persistence/Seeding/Data/ -> SeedData patterns
53
53
 
54
54
  SEED DATA (CRITICAL):
@@ -76,8 +76,8 @@ FRONTEND:
76
76
  | DTOs | `Application/Business/{Application}/{Module}/DTOs/{Entity}Dto.cs` | Response, Create, Update records |
77
77
  | Mapping | `Application/Business/{Application}/{Module}/DTOs/{Entity}MappingExtensions.cs` | Entity <-> DTO mapping |
78
78
  | Permission Constants | `Application/Business/{Application}/{Module}/Permissions.cs` | Compile-time constants |
79
- | EF Configuration | `Infrastructure/Persistence/Configurations/{Module}/{Entity}Configuration.cs` | Table + indexes + HasData |
80
- | Service Implementation | `Infrastructure/Services/{Module}/{Entity}Service.cs` | Business logic |
79
+ | EF Configuration | `Infrastructure/Persistence/Configurations/{Context}/{Application}/{Module}/{Entity}Configuration.cs` | Table + indexes + HasData |
80
+ | Service Implementation | `Infrastructure/Services/{Context}/{Application}/{Module}/{Entity}Service.cs` | Business logic |
81
81
  | Controller | `Api/Controllers/Business/{Application}/{Module}Controller.cs` | NavRoute + RequirePermission |
82
82
 
83
83
  ### 3.2 SeedData Core (CRITICAL -- without these, module is invisible and returns 403)
@@ -156,8 +156,8 @@ From: `feature.specification.seedDataCore` (5 core files), `feature.specificatio
156
156
  **Folder Conventions:**
157
157
  - [ ] Domain entities in `Domain/Business/{Application}/{Module}/`
158
158
  - [ ] Application DTOs in `Application/Business/{Application}/{Module}/DTOs/`
159
- - [ ] Infrastructure configs in `Configurations/{Module}/`
160
- - [ ] Infrastructure services in `Services/{Module}/`
159
+ - [ ] Infrastructure configs in `Configurations/{Context}/{Application}/{Module}/`
160
+ - [ ] Infrastructure services in `Services/{Context}/{Application}/{Module}/`
161
161
  - [ ] Controller in `Controllers/Business/{Application}/`
162
162
 
163
163
  **SeedData Core (CRITICAL):**
@@ -136,7 +136,10 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
136
136
  { key: "tests", category: "test" }
137
137
  ];
138
138
 
139
- const filesToCreate = prdJson.implementation?.filesToCreate || {};
139
+ // Support BOTH formats: nested (from ss derive-prd) and flat (LLM-generated)
140
+ const filesToCreate = prdJson.implementation?.filesToCreate
141
+ || prdJson.filesToCreate
142
+ || {};
140
143
 
141
144
  // 1. Generate tasks from implementation.filesToCreate (primary source)
142
145
  // CRITICAL: Frontend and i18n tasks are CONSOLIDATED into ONE module-level task each.
@@ -175,15 +178,23 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
175
178
  }
176
179
 
177
180
  // Build acceptance criteria from linked FRs and UCs
181
+ // Support both nested (requirements.functionalRequirements) and flat (functionalRequirements) formats
182
+ const allFRs = prdJson.requirements?.functionalRequirements || prdJson.functionalRequirements || [];
178
183
  const linkedFRs = (fileSpec.linkedFRs || [])
179
184
  .map(frId => {
180
- const fr = prdJson.requirements?.functionalRequirements?.find(f => f.id === frId);
185
+ const fr = allFRs.find(f => f.id === frId);
181
186
  return fr ? fr.statement : frId;
182
187
  });
183
- const criteria = linkedFRs.length > 0
188
+ let criteria = linkedFRs.length > 0
184
189
  ? `Implements: ${linkedFRs.join("; ")}`
185
190
  : `File ${fileSpec.path} created and compiles correctly`;
186
191
 
192
+ // Enhance acceptance criteria for API tasks with permission enforcement
193
+ const permMatrix = prdJson.architecture?.permissionMatrix || prdJson.permissionMatrix;
194
+ if (layer.category === "api" && permMatrix?.permissions?.length > 0) {
195
+ criteria += "; MANDATORY: [RequirePermission] attributes on every endpoint (NOT [Authorize])";
196
+ }
197
+
187
198
  tasks.push({
188
199
  id: taskId,
189
200
  description: fileSpec.description
@@ -273,18 +284,250 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
273
284
  taskId++;
274
285
  }
275
286
 
287
+ // 1d. GUARDRAIL: Inject missing layer tasks when filesToCreate is incomplete
288
+ // This handles LLM-generated PRDs that omit layers present in the source data.
289
+ // Uses architecture data (entities, apiEndpoints, sections, wireframes) as fallback.
290
+
291
+ const entities = prdJson.architecture?.entities || prdJson.entities || [];
292
+ const apiEndpoints = prdJson.architecture?.apiEndpoints || prdJson.apiEndpoints || [];
293
+ const sections = prdJson.architecture?.sections || prdJson.sections || [];
294
+ const wireframes = prdJson.specification?.uiWireframes || prdJson.uiWireframes || [];
295
+ const dashboards = prdJson.specification?.dashboards || prdJson.dashboards || [];
296
+
297
+ const hasDomainTasks = tasks.some(t => t.category === 'domain');
298
+ const hasInfraTasks = tasks.some(t => t.category === 'infrastructure');
299
+ const hasApiTasks = tasks.some(t => t.category === 'api');
300
+ const hasFrontendTasks = frontendFiles.length > 0; // Already handled in 1b
301
+ const hasTestTasks = tasks.some(t => t.category === 'test');
302
+
303
+ // INJECT consolidated infrastructure task if missing
304
+ if (!hasInfraTasks && entities.length > 0) {
305
+ const domainDepId = lastIdByCategory["domain"];
306
+ tasks.push({
307
+ id: taskId,
308
+ description: `[infrastructure] Create EF Core configurations, services, and seed data for module ${moduleCode} (${entities.length} entities)`,
309
+ status: "pending",
310
+ category: "infrastructure",
311
+ dependencies: domainDepId ? [domainDepId] : [],
312
+ acceptance_criteria: `EF Core configs for all ${entities.length} entities; Service implementations; DbSet in ExtensionsDbContext; DI registration; Seed data`,
313
+ started_at: null, completed_at: null, iteration: null, commit_hash: null,
314
+ files_changed: { created: [], modified: [] },
315
+ validation: null, error: null, module: moduleCode
316
+ });
317
+ lastIdByCategory["infrastructure"] = taskId;
318
+ taskId++;
319
+ console.log(`⚠️ GUARDRAIL: Injected missing [infrastructure] task from ${entities.length} entities`);
320
+ }
321
+
322
+ // INJECT consolidated API task if missing
323
+ if (!hasApiTasks && apiEndpoints.length > 0) {
324
+ const appDepId = lastIdByCategory["application"] || lastIdByCategory["infrastructure"];
325
+ const permMatrix = prdJson.architecture?.permissionMatrix || prdJson.permissionMatrix;
326
+ const hasPermissions = permMatrix?.permissions?.length > 0;
327
+
328
+ tasks.push({
329
+ id: taskId,
330
+ description: `[api] Create API controllers for module ${moduleCode} (${apiEndpoints.length} endpoints)`,
331
+ status: "pending",
332
+ category: "api",
333
+ dependencies: appDepId ? [appDepId] : [],
334
+ acceptance_criteria: [
335
+ `Controllers for all ${apiEndpoints.length} endpoints`,
336
+ hasPermissions
337
+ ? "MANDATORY: [RequirePermission(Permissions.{Action})] on EVERY endpoint (NOT generic [Authorize])"
338
+ : "Authorization attributes",
339
+ "Swagger docs"
340
+ ].join("; "),
341
+ started_at: null, completed_at: null, iteration: null, commit_hash: null,
342
+ files_changed: { created: [], modified: [] },
343
+ validation: null, error: null, module: moduleCode
344
+ });
345
+ lastIdByCategory["api"] = taskId;
346
+ taskId++;
347
+ console.log(`⚠️ GUARDRAIL: Injected missing [api] task from ${apiEndpoints.length} endpoints`);
348
+ }
349
+
350
+ // CRITICAL: INJECT consolidated frontend task if missing
351
+ // This is the MOST COMMON failure mode — LLM-generated PRDs often omit frontend
352
+ if (!hasFrontendTasks && (sections.length > 0 || wireframes.length > 0 || apiEndpoints.length > 0)) {
353
+ const apiDepId = lastIdByCategory["api"] || lastIdByCategory["application"];
354
+
355
+ // Derive frontend file list from available data sources
356
+ const derivedFrontendFiles = [];
357
+
358
+ // From wireframes → pages
359
+ for (const wf of wireframes) {
360
+ const screenId = wf.screen || wf.id;
361
+ derivedFrontendFiles.push({
362
+ path: `web/src/pages/${moduleCode}/${screenId}.tsx`,
363
+ type: screenId.includes('dashboard') ? 'DashboardPage' : 'Page',
364
+ linkedWireframes: [screenId],
365
+ linkedUCs: wf.linkedUCs || [],
366
+ });
367
+ }
368
+
369
+ // From dashboards → dashboard pages (if not already from wireframes)
370
+ for (const dash of dashboards) {
371
+ const dashId = dash.code || dash.id;
372
+ if (!derivedFrontendFiles.some(f => f.path.includes(dashId))) {
373
+ derivedFrontendFiles.push({
374
+ path: `web/src/pages/${moduleCode}/${dashId}.tsx`,
375
+ type: 'DashboardPage',
376
+ linkedWireframes: [dashId],
377
+ dashboardRef: dashId,
378
+ });
379
+ }
380
+ }
381
+
382
+ // From API endpoints → API client service
383
+ if (apiEndpoints.length > 0) {
384
+ derivedFrontendFiles.push({
385
+ path: `web/src/services/api/${moduleCode}Api.ts`,
386
+ type: 'ApiService',
387
+ });
388
+ }
389
+
390
+ const allWireframeIds = derivedFrontendFiles.flatMap(f => f.linkedWireframes || []);
391
+
392
+ tasks.push({
393
+ id: taskId,
394
+ description: `[frontend] Generate COMPLETE frontend for module ${moduleCode} via MCP scaffold tools (${derivedFrontendFiles.length} files: pages, components, hooks, API client)`,
395
+ status: "pending",
396
+ category: "frontend",
397
+ dependencies: apiDepId ? [apiDepId] : [],
398
+ acceptance_criteria: [
399
+ "MCP scaffold_api_client called → API client + types generated",
400
+ "MCP scaffold_routes called → routes.tsx updated with nested routes inside Layout wrapper",
401
+ allWireframeIds.length > 0 ? "Pages match wireframes: " + allWireframeIds.join(", ") : "Pages created for all UI sections",
402
+ "SmartTable for lists (NOT HTML tables), EntityCard for grids (NOT custom divs)",
403
+ "CSS variables ONLY (NO hardcoded Tailwind colors like bg-blue-600)",
404
+ "useNavigate + useParams for routing, NOT window.location",
405
+ "Loading/error/empty states on all pages",
406
+ "4-language i18n keys (fr, en, it, de)",
407
+ "npm run typecheck passes"
408
+ ].join("; "),
409
+ started_at: null, completed_at: null, iteration: null, commit_hash: null,
410
+ files_changed: { created: derivedFrontendFiles.map(f => f.path), modified: [] },
411
+ validation: null, error: null, module: moduleCode,
412
+ _frontendMeta: {
413
+ wireframes: allWireframeIds,
414
+ filesToCreate: derivedFrontendFiles,
415
+ source: "guardrail-derived"
416
+ }
417
+ });
418
+ lastIdByCategory["frontend"] = taskId;
419
+ taskId++;
420
+ console.log(`⚠️ GUARDRAIL: Injected missing [frontend] task derived from ${wireframes.length} wireframes + ${dashboards.length} dashboards + ${apiEndpoints.length} endpoints`);
421
+
422
+ // Also inject i18n task if not already present
423
+ if (i18nFiles.length === 0) {
424
+ tasks.push({
425
+ id: taskId,
426
+ description: `[i18n] Generate i18n translations for module ${moduleCode} (4 languages: fr, en, it, de)`,
427
+ status: "pending",
428
+ category: "i18n",
429
+ dependencies: [lastIdByCategory["frontend"]],
430
+ acceptance_criteria: "4 JSON files (fr, en, it, de) with identical keys; all UI labels translated",
431
+ started_at: null, completed_at: null, iteration: null, commit_hash: null,
432
+ files_changed: { created: [], modified: [] },
433
+ validation: null, error: null, module: moduleCode
434
+ });
435
+ lastIdByCategory["i18n"] = taskId;
436
+ taskId++;
437
+ }
438
+ }
439
+
440
+ // INJECT consolidated test task if missing
441
+ if (!hasTestTasks && entities.length > 0) {
442
+ const apiDepId = lastIdByCategory["api"] || lastIdByCategory["application"];
443
+ tasks.push({
444
+ id: taskId,
445
+ description: `[test] Create unit and integration tests for module ${moduleCode}`,
446
+ status: "pending",
447
+ category: "test",
448
+ dependencies: apiDepId ? [apiDepId] : [],
449
+ acceptance_criteria: "Domain unit tests + service unit tests + controller integration tests; dotnet test passes; coverage >= 80%",
450
+ started_at: null, completed_at: null, iteration: null, commit_hash: null,
451
+ files_changed: { created: [], modified: [] },
452
+ validation: null, error: null, module: moduleCode
453
+ });
454
+ lastIdByCategory["test"] = taskId;
455
+ taskId++;
456
+ console.log(`⚠️ GUARDRAIL: Injected missing [test] task`);
457
+ }
458
+
459
+ // 1e. GUARDRAIL: Inject seedData tasks when core seed data exists but filesToCreate.seedData is empty
460
+ // This is the MOST CRITICAL guardrail — without core seed data, the application has:
461
+ // - No navigation menu entries
462
+ // - No RBAC permissions in the database
463
+ // - No role-permission mappings
464
+ // - Controllers accessible to ANY authenticated user
465
+ const coreSeedData = prdJson.seedData?.core
466
+ || prdJson.seedDataCore
467
+ || prdJson.specification?.seedDataCore;
468
+ const hasSeedDataTasks = (filesToCreate["seedData"]?.length ?? 0) > 0;
469
+ const hasSeedDataInTasks = tasks.some(t =>
470
+ t.description?.includes("SeedData") || t.description?.includes("seed data"));
471
+
472
+ if (coreSeedData && !hasSeedDataTasks && !hasSeedDataInTasks) {
473
+ const infraDepId = lastIdByCategory["infrastructure"] || lastIdByCategory["domain"];
474
+
475
+ // Derive navigation/permissions/roles from coreSeedData
476
+ const navModules = coreSeedData.navigationModules || coreSeedData.navigation || [];
477
+ const permissions = coreSeedData.permissions || [];
478
+ const rolePerms = coreSeedData.rolePermissions || [];
479
+
480
+ // Inject consolidated core seedData task
481
+ tasks.push({
482
+ id: taskId,
483
+ description: `[infrastructure] Create core seed data files for module ${moduleCode}: NavigationModuleSeedData, PermissionsSeedData, RolesSeedData, SeedConstants`,
484
+ status: "pending",
485
+ category: "infrastructure",
486
+ dependencies: infraDepId ? [infraDepId] : [],
487
+ acceptance_criteria: [
488
+ `NavigationModuleSeedData.cs with ${navModules.length} navigation module(s)`,
489
+ `PermissionsSeedData.cs with ${permissions.length} permission(s) from seedData.core`,
490
+ `RolesSeedData.cs with ${rolePerms.length} role-permission mapping(s)`,
491
+ "SeedConstants.cs with deterministic GUIDs",
492
+ "All seed data classes are static with GetSeedData() method"
493
+ ].join("; "),
494
+ started_at: null, completed_at: null, iteration: null, commit_hash: null,
495
+ files_changed: { created: [
496
+ `src/Infrastructure/Persistence/Seeding/Data/${moduleCode}/NavigationModuleSeedData.cs`,
497
+ `src/Infrastructure/Persistence/Seeding/Data/${moduleCode}/PermissionsSeedData.cs`,
498
+ `src/Infrastructure/Persistence/Seeding/Data/${moduleCode}/RolesSeedData.cs`,
499
+ "src/Infrastructure/Persistence/Seeding/Data/SeedConstants.cs"
500
+ ], modified: [] },
501
+ validation: null, error: null, module: moduleCode,
502
+ _seedDataMeta: { source: "guardrail-derived", coreSeedData }
503
+ });
504
+ lastIdByCategory["infrastructure"] = taskId;
505
+ taskId++;
506
+ console.log(`GUARDRAIL: Injected missing [infrastructure] core seed data task from seedData.core (${navModules.length} nav, ${permissions.length} perms, ${rolePerms.length} roles)`);
507
+ }
508
+
276
509
  // 2. Add IClientSeedDataProvider task for client projects (ExtensionsDbContext)
277
510
  // This is MANDATORY - without it, core seed data (navigation, permissions, roles) is dead code
278
511
  const hasClientSeedData = filesToCreate["seedData"]?.some(f =>
279
512
  f.type === "IClientSeedDataProvider" || f.path?.includes("SeedDataProvider"));
280
- if (!hasClientSeedData && filesToCreate["seedData"]?.length > 0) {
513
+ const hasProviderInTasks = tasks.some(t =>
514
+ t.description?.includes("IClientSeedDataProvider") || t.description?.includes("SeedDataProvider"));
515
+
516
+ // Trigger when: seedData files exist OR coreSeedData exists (fallback for incomplete PRDs)
517
+ if (!hasClientSeedData && !hasProviderInTasks &&
518
+ (filesToCreate["seedData"]?.length > 0 || coreSeedData)) {
281
519
  tasks.push({
282
520
  id: taskId,
283
- description: `[infrastructure] Create IClientSeedDataProvider to inject core seed data at runtime`,
521
+ description: `[infrastructure] Create IClientSeedDataProvider to inject core seed data (navigation, permissions, roles) at runtime`,
284
522
  status: "pending",
285
523
  category: "infrastructure",
286
524
  dependencies: [lastIdByCategory["infrastructure"] || lastIdByCategory["domain"]].filter(Boolean),
287
- acceptance_criteria: "IClientSeedDataProvider implements SeedNavigationAsync, SeedPermissionsAsync, SeedRolePermissionsAsync; registered in DI",
525
+ acceptance_criteria: [
526
+ "Implements IClientSeedDataProvider with SeedNavigationAsync, SeedPermissionsAsync, SeedRolePermissionsAsync",
527
+ "Registered in DI: services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()",
528
+ "All methods are idempotent (check existence before inserting)",
529
+ "Consumes seed data from NavigationModuleSeedData, PermissionsSeedData, RolesSeedData"
530
+ ].join("; "),
288
531
  started_at: null, completed_at: null, iteration: null, commit_hash: null,
289
532
  files_changed: { created: ["src/Infrastructure/Persistence/Seeding/{AppPascalName}SeedDataProvider.cs"], modified: ["src/Infrastructure/DependencyInjection.cs"] },
290
533
  validation: null, error: null, module: moduleCode