@atlashub/smartstack-cli 3.4.0 → 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.
@@ -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