@atlashub/smartstack-cli 3.23.0 → 3.25.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 (36) hide show
  1. package/dist/index.js +5 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +96 -24
  4. package/dist/mcp-entry.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/mcp-scaffolding/component.tsx.hbs +21 -1
  7. package/templates/skills/apex/references/smartstack-api.md +174 -5
  8. package/templates/skills/apex/references/smartstack-frontend.md +1101 -0
  9. package/templates/skills/apex/references/smartstack-layers.md +81 -5
  10. package/templates/skills/apex/steps/step-01-analyze.md +27 -3
  11. package/templates/skills/apex/steps/step-02-plan.md +5 -1
  12. package/templates/skills/apex/steps/step-03-execute.md +47 -5
  13. package/templates/skills/apex/steps/step-04-validate.md +300 -0
  14. package/templates/skills/apex/steps/step-05-examine.md +7 -0
  15. package/templates/skills/apex/steps/step-07-tests.md +19 -0
  16. package/templates/skills/business-analyse/_shared.md +6 -6
  17. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +1 -1
  18. package/templates/skills/business-analyse/questionnaire/07-ui.md +3 -3
  19. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +1 -1
  20. package/templates/skills/business-analyse/references/entity-architecture-decision.md +3 -3
  21. package/templates/skills/business-analyse/references/handoff-file-templates.md +13 -5
  22. package/templates/skills/business-analyse/references/spec-auto-inference.md +14 -14
  23. package/templates/skills/business-analyse/steps/step-01-cadrage.md +2 -2
  24. package/templates/skills/business-analyse/steps/step-02-decomposition.md +1 -1
  25. package/templates/skills/business-analyse/steps/step-03a1-setup.md +2 -2
  26. package/templates/skills/business-analyse/steps/step-03b-ui.md +2 -1
  27. package/templates/skills/business-analyse/steps/step-05a-handoff.md +15 -4
  28. package/templates/skills/business-analyse/templates/tpl-frd.md +2 -2
  29. package/templates/skills/business-analyse/templates-frd.md +2 -2
  30. package/templates/skills/efcore/steps/migration/step-02-create.md +14 -1
  31. package/templates/skills/ralph-loop/references/category-rules.md +71 -9
  32. package/templates/skills/ralph-loop/references/compact-loop.md +3 -3
  33. package/templates/skills/ralph-loop/references/core-seed-data.md +10 -0
  34. package/templates/skills/ralph-loop/steps/step-02-execute.md +190 -1
  35. package/templates/skills/validate-feature/steps/step-01-compile.md +4 -1
  36. package/templates/skills/validate-feature/steps/step-05-db-validation.md +86 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "3.23.0",
3
+ "version": "3.25.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -174,7 +174,27 @@ export const {{name}}: React.FC<{{name}}Props> = ({
174
174
 
175
175
  {/* Form */}
176
176
  <form onSubmit={handleSubmit} className="p-6 space-y-6">
177
- {/* TODO: Add form fields */}
177
+ {/*
178
+ TODO: Add form fields based on entity properties.
179
+ IMPORTANT — Field type mapping:
180
+ - string properties → <input type="text" />
181
+ - bool properties → <input type="checkbox" />
182
+ - number properties → <input type="number" />
183
+ - DateTime properties → <input type="date" />
184
+ - Guid FK properties (e.g., EmployeeId, DepartmentId) → <EntityLookup /> (NEVER plain text input!)
185
+
186
+ For FK fields, use EntityLookup from @/components/ui/EntityLookup:
187
+ <EntityLookup
188
+ apiEndpoint="/api/{related-entity-route}"
189
+ value={data.relatedEntityId}
190
+ onChange={(id) => handleChange('relatedEntityId', id)}
191
+ label="Related Entity"
192
+ mapOption={(item) => ({ id: item.id, label: item.name, sublabel: item.code })}
193
+ required
194
+ />
195
+
196
+ See smartstack-frontend.md section 6 for the full EntityLookup pattern.
197
+ */}
178
198
  <div className="text-[var(--text-secondary)] text-center py-8">
179
199
  Add your form fields here
180
200
  </div>
@@ -228,13 +228,33 @@ public class {Name}Service : I{Name}Service
228
228
  _logger = logger;
229
229
  }
230
230
 
231
- public async Task<List<{Name}ResponseDto>> GetAllAsync(CancellationToken ct)
231
+ public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
232
+ string? search = null,
233
+ int page = 1,
234
+ int pageSize = 20,
235
+ CancellationToken ct = default)
232
236
  {
233
- return await _db.{Name}s
237
+ var query = _db.{Name}s
234
238
  .Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY tenant filter
235
- .AsNoTracking()
239
+ .AsNoTracking();
240
+
241
+ // Search filter — enables EntityLookup on frontend
242
+ if (!string.IsNullOrWhiteSpace(search))
243
+ {
244
+ query = query.Where(x =>
245
+ x.Name.Contains(search) ||
246
+ x.Code.Contains(search));
247
+ }
248
+
249
+ var totalCount = await query.CountAsync(ct);
250
+ var items = await query
251
+ .OrderBy(x => x.Name)
252
+ .Skip((page - 1) * pageSize)
253
+ .Take(pageSize)
236
254
  .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
237
255
  .ToListAsync(ct);
256
+
257
+ return new PaginatedResult<{Name}ResponseDto>(items, totalCount, page, pageSize);
238
258
  }
239
259
 
240
260
  public async Task<{Name}ResponseDto?> GetByIdAsync(Guid id, CancellationToken ct)
@@ -313,8 +333,12 @@ public class {Name}Controller : ControllerBase
313
333
 
314
334
  [HttpGet]
315
335
  [RequirePermission(Permissions.{Module}.Read)]
316
- public async Task<ActionResult<List<{Name}ResponseDto>>> GetAll(CancellationToken ct)
317
- => Ok(await _service.GetAllAsync(ct));
336
+ public async Task<ActionResult<PaginatedResult<{Name}ResponseDto>>> GetAll(
337
+ [FromQuery] string? search = null,
338
+ [FromQuery] int page = 1,
339
+ [FromQuery] int pageSize = 20,
340
+ CancellationToken ct = default)
341
+ => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
318
342
 
319
343
  [HttpGet("{id:guid}")]
320
344
  [RequirePermission(Permissions.{Module}.Read)]
@@ -479,3 +503,148 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
479
503
  | Route `"humanresources"` in seed data | Must be full path `"/business/human-resources"` |
480
504
  | Route without leading `/` | All routes must start with `/` |
481
505
  | `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
506
+ | `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
507
+ | FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
508
+
509
+ ---
510
+
511
+ ## Critical Anti-Patterns (with code examples)
512
+
513
+ > **These are the most common and dangerous mistakes.** Each one has been observed in production code generation.
514
+
515
+ ### Anti-Pattern 1: HasQueryFilter with `!= Guid.Empty` (SECURITY — OWASP A01)
516
+
517
+ The `HasQueryFilter` in EF Core should use **runtime tenant resolution**, NOT a static comparison against `Guid.Empty`.
518
+
519
+ **INCORRECT — Does NOT isolate tenants:**
520
+ ```csharp
521
+ // WRONG: This only excludes empty GUIDs — ALL tenant data is still visible to everyone!
522
+ public void Configure(EntityTypeBuilder<MyEntity> builder)
523
+ {
524
+ builder.HasQueryFilter(e => e.TenantId != Guid.Empty);
525
+ }
526
+ ```
527
+
528
+ **CORRECT — Tenant isolation via service:**
529
+ ```csharp
530
+ // CORRECT: In SmartStack, tenant filtering is done in the SERVICE layer, not via HasQueryFilter.
531
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
532
+ {
533
+ var query = _db.MyEntities
534
+ .Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY runtime filter
535
+ .AsNoTracking();
536
+ // ...
537
+ }
538
+ ```
539
+
540
+ **Why it's wrong:** `HasQueryFilter(e => e.TenantId != Guid.Empty)` is a **static filter** — it only removes records with empty GUIDs. It does NOT restrict data to the current tenant. This is an **OWASP A01 Broken Access Control** vulnerability.
541
+
542
+ ---
543
+
544
+ ### Anti-Pattern 2: `List<T>` instead of `PaginatedResult<T>` for GetAll
545
+
546
+ **INCORRECT — No pagination:**
547
+ ```csharp
548
+ // WRONG: Returns all records at once — no pagination, no totalCount
549
+ public async Task<List<MyEntityDto>> GetAllAsync(CancellationToken ct)
550
+ {
551
+ return await _db.MyEntities
552
+ .Where(x => x.TenantId == _currentUser.TenantId)
553
+ .Select(x => new MyEntityDto(x.Id, x.Code, x.Name))
554
+ .ToListAsync(ct);
555
+ }
556
+ ```
557
+
558
+ **CORRECT — Paginated with search:**
559
+ ```csharp
560
+ // CORRECT: Returns PaginatedResult<T> with search, page, pageSize
561
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(
562
+ string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
563
+ {
564
+ var query = _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).AsNoTracking();
565
+ if (!string.IsNullOrWhiteSpace(search))
566
+ query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
567
+ var totalCount = await query.CountAsync(ct);
568
+ var items = await query.OrderBy(x => x.Name).Skip((page - 1) * pageSize).Take(pageSize)
569
+ .Select(x => new MyEntityDto(x.Id, x.Code, x.Name, x.CreatedAt)).ToListAsync(ct);
570
+ return new PaginatedResult<MyEntityDto>(items, totalCount, page, pageSize);
571
+ }
572
+ ```
573
+
574
+ **Why it's wrong:** `List<T>` loads ALL records into memory. It also breaks `EntityLookup` which requires `{ items, totalCount }` response format.
575
+
576
+ ---
577
+
578
+ ### Anti-Pattern 3: Missing `IAuditableEntity` on tenant entities
579
+
580
+ **INCORRECT — No audit trail:**
581
+ ```csharp
582
+ // WRONG: Tenant entity without IAuditableEntity
583
+ public class MyEntity : BaseEntity, ITenantEntity
584
+ {
585
+ public Guid TenantId { get; private set; }
586
+ public string Code { get; private set; } = null!;
587
+ }
588
+ ```
589
+
590
+ **CORRECT — Always pair ITenantEntity with IAuditableEntity:**
591
+ ```csharp
592
+ public class MyEntity : BaseEntity, ITenantEntity, IAuditableEntity
593
+ {
594
+ public Guid TenantId { get; private set; }
595
+ public string? CreatedBy { get; set; }
596
+ public string? UpdatedBy { get; set; }
597
+ public string Code { get; private set; } = null!;
598
+ }
599
+ ```
600
+
601
+ **Why it's wrong:** Without `IAuditableEntity`, there is no record of who created or modified data. Mandatory for compliance in multi-tenant environments.
602
+
603
+ ---
604
+
605
+ ### Anti-Pattern 4: Code auto-generation with `Count() + 1`
606
+
607
+ **INCORRECT — Race condition:**
608
+ ```csharp
609
+ // WRONG: Two concurrent requests get the same count
610
+ var count = await _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).CountAsync(ct);
611
+ return $"ENT{(count + 1):D4}";
612
+ ```
613
+
614
+ **CORRECT — Use `GenerateNextCodeAsync()` (atomic):**
615
+ ```csharp
616
+ var code = await GenerateNextCodeAsync("MyEntity", _currentUser.TenantId, ct);
617
+ ```
618
+
619
+ **Why it's wrong:** `Count() + 1` causes **race conditions** — concurrent requests generate duplicate codes.
620
+
621
+ ---
622
+
623
+ ### Anti-Pattern 5: Missing Update validator
624
+
625
+ **INCORRECT — Only CreateValidator:**
626
+ ```csharp
627
+ public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto>
628
+ {
629
+ public CreateMyEntityDtoValidator()
630
+ {
631
+ RuleFor(x => x.Code).NotEmpty().MaximumLength(100);
632
+ RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
633
+ }
634
+ }
635
+ // No UpdateMyEntityDtoValidator exists!
636
+ ```
637
+
638
+ **CORRECT — Always create validators in pairs:**
639
+ ```csharp
640
+ public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto> { /* ... */ }
641
+ public class UpdateMyEntityDtoValidator : AbstractValidator<UpdateMyEntityDto>
642
+ {
643
+ public UpdateMyEntityDtoValidator()
644
+ {
645
+ RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
646
+ }
647
+ }
648
+ ```
649
+
650
+ **Why it's wrong:** Without an `UpdateValidator`, the Update endpoint accepts **any data without validation**.