@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.
- package/dist/index.js +161 -16
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +4 -3
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/project/appsettings.json.template +12 -2
- package/templates/skills/_shared.md +1 -1
- package/templates/skills/application/steps/step-04-backend.md +4 -4
- package/templates/skills/application/templates-backend.md +4 -4
- package/templates/skills/business-analyse/SKILL.md +47 -143
- package/templates/skills/business-analyse/_architecture.md +3 -3
- package/templates/skills/business-analyse/_shared.md +1 -1
- package/templates/skills/business-analyse/html/ba-interactive.html +269 -28
- package/templates/skills/business-analyse/schemas/application-schema.json +3 -4
- package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +1 -2
- package/templates/skills/business-analyse/steps/step-00-init.md +130 -398
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +6 -14
- package/templates/skills/business-analyse/steps/step-05b-deploy.md +65 -69
- package/templates/skills/business-analyse/templates/tpl-handoff.md +9 -19
- package/templates/skills/business-analyse/templates/tpl-launch-displays.md +23 -128
- package/templates/skills/business-analyse/templates-frd.md +3 -19
- package/templates/skills/ralph-loop/steps/step-01-task.md +249 -6
- package/templates/skills/business-analyse/steps/step-06-extract.md +0 -648
|
@@ -136,7 +136,10 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
|
|
|
136
136
|
{ key: "tests", category: "test" }
|
|
137
137
|
];
|
|
138
138
|
|
|
139
|
-
|
|
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 =
|
|
185
|
+
const fr = allFRs.find(f => f.id === frId);
|
|
181
186
|
return fr ? fr.statement : frId;
|
|
182
187
|
});
|
|
183
|
-
|
|
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
|
-
|
|
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:
|
|
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
|