@atlashub/smartstack-cli 3.23.0 → 3.24.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 (32) hide show
  1. package/dist/mcp-entry.mjs +56 -15
  2. package/dist/mcp-entry.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/mcp-scaffolding/component.tsx.hbs +21 -1
  5. package/templates/skills/apex/references/smartstack-api.md +31 -5
  6. package/templates/skills/apex/references/smartstack-frontend.md +1081 -0
  7. package/templates/skills/apex/references/smartstack-layers.md +81 -5
  8. package/templates/skills/apex/steps/step-01-analyze.md +27 -3
  9. package/templates/skills/apex/steps/step-02-plan.md +5 -1
  10. package/templates/skills/apex/steps/step-03-execute.md +43 -3
  11. package/templates/skills/apex/steps/step-04-validate.md +159 -0
  12. package/templates/skills/apex/steps/step-05-examine.md +7 -0
  13. package/templates/skills/apex/steps/step-07-tests.md +19 -0
  14. package/templates/skills/business-analyse/_shared.md +6 -6
  15. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +1 -1
  16. package/templates/skills/business-analyse/questionnaire/07-ui.md +3 -3
  17. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +1 -1
  18. package/templates/skills/business-analyse/references/entity-architecture-decision.md +3 -3
  19. package/templates/skills/business-analyse/references/handoff-file-templates.md +13 -5
  20. package/templates/skills/business-analyse/references/spec-auto-inference.md +14 -14
  21. package/templates/skills/business-analyse/steps/step-01-cadrage.md +2 -2
  22. package/templates/skills/business-analyse/steps/step-02-decomposition.md +1 -1
  23. package/templates/skills/business-analyse/steps/step-03a1-setup.md +2 -2
  24. package/templates/skills/business-analyse/steps/step-03b-ui.md +2 -1
  25. package/templates/skills/business-analyse/steps/step-05a-handoff.md +15 -4
  26. package/templates/skills/business-analyse/templates/tpl-frd.md +2 -2
  27. package/templates/skills/business-analyse/templates-frd.md +2 -2
  28. package/templates/skills/ralph-loop/references/category-rules.md +45 -7
  29. package/templates/skills/ralph-loop/references/compact-loop.md +2 -2
  30. package/templates/skills/ralph-loop/references/core-seed-data.md +10 -0
  31. package/templates/skills/ralph-loop/steps/step-02-execute.md +110 -1
  32. package/templates/skills/validate-feature/steps/step-05-db-validation.md +86 -1
@@ -34500,6 +34500,8 @@ public interface I{{name}}Service
34500
34500
  const implementationTemplate = `using System.Threading;
34501
34501
  using System.Threading.Tasks;
34502
34502
  using System.Collections.Generic;
34503
+ using System.Linq;
34504
+ using Microsoft.EntityFrameworkCore;
34503
34505
  using Microsoft.Extensions.Logging;
34504
34506
  using SmartStack.Application.Common.Interfaces.Identity;
34505
34507
  using SmartStack.Application.Common.Interfaces.Persistence;
@@ -34509,6 +34511,7 @@ namespace {{implNamespace}};
34509
34511
  /// <summary>
34510
34512
  /// Service implementation for {{name}} operations.
34511
34513
  /// IMPORTANT: All queries MUST filter by _currentUser.TenantId for multi-tenant isolation.
34514
+ /// IMPORTANT: GetAllAsync MUST support search parameter for frontend EntityLookup component.
34512
34515
  /// </summary>
34513
34516
  public class {{name}}Service : I{{name}}Service
34514
34517
  {
@@ -34532,6 +34535,10 @@ public class {{name}}Service : I{{name}}Service
34532
34535
  {
34533
34536
  _logger.LogInformation("Executing {{this}} for tenant {TenantId}", _currentUser.TenantId);
34534
34537
  // TODO: Implement {{this}} \u2014 ALL queries must filter by _currentUser.TenantId
34538
+ // IMPORTANT: GetAllAsync MUST accept (string? search, int page, int pageSize) parameters
34539
+ // to enable EntityLookup search on the frontend. Example:
34540
+ // if (!string.IsNullOrWhiteSpace(search))
34541
+ // query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
34535
34542
  await Task.CompletedTask;
34536
34543
  throw new NotImplementedException();
34537
34544
  }
@@ -35344,6 +35351,27 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
35344
35351
  const componentImportPath = hierarchy.context && hierarchy.module ? `@/components/${hierarchy.context.toLowerCase()}/${hierarchy.module.toLowerCase()}/${name}` : `./components/${name}`;
35345
35352
  result.instructions.push(`import { ${name} } from '${componentImportPath}';`);
35346
35353
  result.instructions.push(`import { use${name} } from '@/hooks/use${name}';`);
35354
+ result.instructions.push("");
35355
+ result.instructions.push("### IMPORTANT: Foreign Key Fields in Forms");
35356
+ result.instructions.push("If this entity has FK fields (e.g., EmployeeId, DepartmentId):");
35357
+ result.instructions.push("- NEVER render FK Guid fields as plain text inputs");
35358
+ result.instructions.push("- ALWAYS use EntityLookup component from @/components/ui/EntityLookup");
35359
+ result.instructions.push("- EntityLookup provides searchable dropdown with API-backed search");
35360
+ result.instructions.push("- See smartstack-frontend.md section 6 for the full EntityLookup pattern");
35361
+ result.instructions.push("");
35362
+ result.instructions.push("Example:");
35363
+ result.instructions.push("```tsx");
35364
+ result.instructions.push('import { EntityLookup } from "@/components/ui/EntityLookup";');
35365
+ result.instructions.push("");
35366
+ result.instructions.push("<EntityLookup");
35367
+ result.instructions.push(' apiEndpoint="/api/{context}/{app}/{related-entity}"');
35368
+ result.instructions.push(" value={formData.relatedEntityId}");
35369
+ result.instructions.push(' onChange={(id) => handleChange("relatedEntityId", id)}');
35370
+ result.instructions.push(' label="Related Entity"');
35371
+ result.instructions.push(" mapOption={(item) => ({ id: item.id, label: item.name, sublabel: item.code })}");
35372
+ result.instructions.push(" required");
35373
+ result.instructions.push("/>");
35374
+ result.instructions.push("```");
35347
35375
  }
35348
35376
  async function scaffoldTest(name, options, structure, config2, result, dryRun = false) {
35349
35377
  const isSystemEntity = options?.isSystemEntity || false;
@@ -56982,7 +57010,12 @@ async function scaffoldRoutes(input, config2) {
56982
57010
  result.instructions.push("");
56983
57011
  result.instructions.push("Add routes to `contextRoutes.{context}[]` with **RELATIVE** paths (no leading `/`):");
56984
57012
  result.instructions.push("");
57013
+ result.instructions.push("**IMPORTANT:** Pages are lazy-loaded. Use `<Suspense fallback={<PageLoader />}>` wrapper.");
57014
+ result.instructions.push("");
56985
57015
  result.instructions.push("```tsx");
57016
+ result.instructions.push("import { lazy, Suspense } from 'react';");
57017
+ result.instructions.push("import { PageLoader } from '@/components/ui/PageLoader';");
57018
+ result.instructions.push("");
56986
57019
  result.instructions.push("const contextRoutes: ContextRouteExtensions = {");
56987
57020
  for (const [context, applications] of Object.entries(routeTree)) {
56988
57021
  result.instructions.push(` ${context}: [`);
@@ -56991,7 +57024,7 @@ async function scaffoldRoutes(input, config2) {
56991
57024
  const modulePath = route.navRoute.split(".").slice(1).join("/");
56992
57025
  const pageEntry = pageFiles.get(route.navRoute);
56993
57026
  const component = pageEntry?.[0]?.componentName || `${route.navRoute.split(".").map(capitalize).join("")}Page`;
56994
- result.instructions.push(` { path: '${modulePath}', element: <${component} /> },`);
57027
+ result.instructions.push(` { path: '${modulePath}', element: <Suspense fallback={<PageLoader />}><${component} /></Suspense> },`);
56995
57028
  }
56996
57029
  }
56997
57030
  result.instructions.push(" ],");
@@ -57004,7 +57037,8 @@ async function scaffoldRoutes(input, config2) {
57004
57037
  result.instructions.push("");
57005
57038
  result.instructions.push('### Pattern B: JSX Routes (if App.tsx uses `<Route path="/{context}" element={<{Layout} />}>`)');
57006
57039
  result.instructions.push("");
57007
- result.instructions.push("Insert `<Route>` children INSIDE the appropriate Layout wrapper:");
57040
+ result.instructions.push("Insert `<Route>` children INSIDE the appropriate Layout wrapper.");
57041
+ result.instructions.push("**IMPORTANT:** Use `<Suspense fallback={<PageLoader />}>` for lazy-loaded pages.");
57008
57042
  result.instructions.push("");
57009
57043
  for (const [context, applications] of Object.entries(routeTree)) {
57010
57044
  const layoutName = getLayoutName(context);
@@ -57016,7 +57050,7 @@ async function scaffoldRoutes(input, config2) {
57016
57050
  const modulePath = route.navRoute.split(".").slice(1).join("/");
57017
57051
  const pageEntry = pageFiles.get(route.navRoute);
57018
57052
  const component = pageEntry?.[0]?.componentName || `${route.navRoute.split(".").map(capitalize).join("")}Page`;
57019
- result.instructions.push(`<Route path="${modulePath}" element={<${component} />} />`);
57053
+ result.instructions.push(`<Route path="${modulePath}" element={<Suspense fallback={<PageLoader />}><${component} /></Suspense>} />`);
57020
57054
  }
57021
57055
  }
57022
57056
  result.instructions.push("```");
@@ -57255,27 +57289,30 @@ function generateRouterConfig(routes, includeGuards) {
57255
57289
  " */",
57256
57290
  "",
57257
57291
  "import { createBrowserRouter, RouteObject } from 'react-router-dom';",
57258
- "import { ROUTES } from './navRoutes.generated';"
57292
+ "import { lazy, Suspense } from 'react';",
57293
+ "import { ROUTES } from './navRoutes.generated';",
57294
+ "import { PageLoader } from '@/components/ui/PageLoader';"
57259
57295
  ];
57260
57296
  if (includeGuards) {
57261
57297
  lines.push("import { ProtectedRoute, PermissionGuard } from './guards';");
57262
57298
  }
57263
57299
  const contexts = Object.keys(routeTree);
57264
57300
  for (const context of contexts) {
57265
- lines.push(`import { ${capitalize(context)}Layout } from '../layouts/${capitalize(context)}Layout';`);
57301
+ const name = `${capitalize(context)}Layout`;
57302
+ lines.push(`const ${name} = lazy(() => import('../layouts/${name}').then(m => ({ default: m.${name} })));`);
57266
57303
  }
57267
57304
  lines.push("");
57268
- lines.push("// Page imports - customize these paths");
57305
+ lines.push("// Page imports - lazy loaded (customize paths)");
57269
57306
  for (const route of routes) {
57270
57307
  const pageName = route.navRoute.split(".").map(capitalize).join("");
57271
- lines.push(`// import { ${pageName}Page } from '../pages/${pageName}Page';`);
57308
+ lines.push(`// const ${pageName}Page = lazy(() => import('../pages/${pageName}Page').then(m => ({ default: m.${pageName}Page })));`);
57272
57309
  }
57273
57310
  lines.push("");
57274
57311
  lines.push("const routes: RouteObject[] = [");
57275
57312
  for (const [context, applications] of Object.entries(routeTree)) {
57276
57313
  lines.push(" {");
57277
57314
  lines.push(` path: '${context}',`);
57278
- lines.push(` element: <${capitalize(context)}Layout />,`);
57315
+ lines.push(` element: <Suspense fallback={<PageLoader />}><${capitalize(context)}Layout /></Suspense>,`);
57279
57316
  lines.push(" children: [");
57280
57317
  for (const [app, modules] of Object.entries(applications)) {
57281
57318
  lines.push(" {");
@@ -57503,7 +57540,9 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57503
57540
  "",
57504
57541
  "import type { RouteObject } from 'react-router-dom';",
57505
57542
  "import type { ContextRouteExtensions } from '@atlashub/smartstack';",
57506
- "import { Navigate } from 'react-router-dom';"
57543
+ "import { lazy, Suspense } from 'react';",
57544
+ "import { Navigate } from 'react-router-dom';",
57545
+ "import { PageLoader } from '@/components/ui/PageLoader';"
57507
57546
  ];
57508
57547
  if (includeGuards) {
57509
57548
  lines.push("import { ROUTES } from './navRoutes.generated';");
@@ -57516,7 +57555,9 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57516
57555
  if (pageEntries) {
57517
57556
  for (const entry of pageEntries) {
57518
57557
  if (!importedComponents.has(entry.componentName)) {
57519
- lines.push(`import { ${entry.componentName} } from '${entry.importPath}';`);
57558
+ lines.push(`const ${entry.componentName} = lazy(() =>`);
57559
+ lines.push(` import('${entry.importPath}').then(m => ({ default: m.${entry.componentName} }))`);
57560
+ lines.push(");");
57520
57561
  importedComponents.add(entry.componentName);
57521
57562
  }
57522
57563
  }
@@ -57525,7 +57566,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57525
57566
  for (const route of routes) {
57526
57567
  if (!pageFiles.has(route.navRoute)) {
57527
57568
  const pageName = route.navRoute.split(".").map(capitalize).join("") + "Page";
57528
- lines.push(`// TODO: import { ${pageName} } from '@/pages/...';`);
57569
+ lines.push(`// TODO: const ${pageName} = lazy(() => import('@/pages/...').then(m => ({ default: m.${pageName} })));`);
57529
57570
  }
57530
57571
  }
57531
57572
  lines.push("");
@@ -57554,7 +57595,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57554
57595
  lines.push(` {`);
57555
57596
  lines.push(` path: '${modulePath}',`);
57556
57597
  if (hasRealPage) {
57557
- lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard>,`);
57598
+ lines.push(` element: <Suspense fallback={<PageLoader />}><PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard></Suspense>,`);
57558
57599
  } else {
57559
57600
  lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><div>TODO: ${component}</div></PermissionGuard>,`);
57560
57601
  }
@@ -57562,7 +57603,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57562
57603
  } else {
57563
57604
  lines.push(` {`);
57564
57605
  lines.push(` path: '${modulePath}',`);
57565
- lines.push(` element: ${hasRealPage ? `<${component} />` : `<div>TODO: ${component}</div>`},`);
57606
+ lines.push(` element: ${hasRealPage ? `<Suspense fallback={<PageLoader />}><${component} /></Suspense>` : `<div>TODO: ${component}</div>`},`);
57566
57607
  lines.push(` },`);
57567
57608
  }
57568
57609
  }
@@ -57579,7 +57620,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57579
57620
  lines.push(` {`);
57580
57621
  lines.push(` path: '${fullPath}',`);
57581
57622
  if (hasRealPage) {
57582
- lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard>,`);
57623
+ lines.push(` element: <Suspense fallback={<PageLoader />}><PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard></Suspense>,`);
57583
57624
  } else {
57584
57625
  lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><div>TODO: ${component}</div></PermissionGuard>,`);
57585
57626
  }
@@ -57587,7 +57628,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
57587
57628
  } else {
57588
57629
  lines.push(` {`);
57589
57630
  lines.push(` path: '${fullPath}',`);
57590
- lines.push(` element: ${hasRealPage ? `<${component} />` : `<div>TODO: ${component}</div>`},`);
57631
+ lines.push(` element: ${hasRealPage ? `<Suspense fallback={<PageLoader />}><${component} /></Suspense>` : `<div>TODO: ${component}</div>`},`);
57591
57632
  lines.push(` },`);
57592
57633
  }
57593
57634
  }