@atlashub/smartstack-cli 3.22.0 → 3.23.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/mcp-entry.mjs +87 -159
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/skills/apex/SKILL.md +21 -0
- package/templates/skills/apex/references/smartstack-api.md +481 -0
- package/templates/skills/apex/references/smartstack-layers.md +85 -15
- package/templates/skills/apex/steps/step-00-init.md +27 -14
- package/templates/skills/apex/steps/step-01-analyze.md +18 -0
- package/templates/skills/apex/steps/step-03-execute.md +8 -6
- package/templates/skills/apex/steps/step-04-validate.md +92 -0
- package/templates/skills/apex/steps/step-07-tests.md +29 -5
package/package.json
CHANGED
|
@@ -91,6 +91,7 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
91
91
|
|
|
92
92
|
| File | Purpose | Loaded by |
|
|
93
93
|
|------|---------|-----------|
|
|
94
|
+
| `references/smartstack-api.md` | BaseEntity, interfaces, entity/config/controller patterns | step-01, step-03 |
|
|
94
95
|
| `references/smartstack-layers.md` | Layer execution rules, skill/MCP mapping, seed data | step-02, step-03 |
|
|
95
96
|
| `references/agent-teams-protocol.md` | TeamCreate, coordination, shutdown protocol | step-01, step-03 |
|
|
96
97
|
</reference_files>
|
|
@@ -108,6 +109,26 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
108
109
|
|
|
109
110
|
</execution_rules>
|
|
110
111
|
|
|
112
|
+
<error_handling>
|
|
113
|
+
|
|
114
|
+
## Error Handling Strategy
|
|
115
|
+
|
|
116
|
+
| Error | Action | Max Retries |
|
|
117
|
+
|-------|--------|-------------|
|
|
118
|
+
| `dotnet build` fails | Identify error, fix via skill/MCP, rebuild | 3 |
|
|
119
|
+
| `npm run typecheck` fails | Fix TypeScript error, retry | 3 |
|
|
120
|
+
| MCP unavailable | Degraded mode: use `smartstack-api.md` as sole reference, no MCP validation | — |
|
|
121
|
+
| File lock (MSB3021) | Auto-use `--output /tmp/{project}_build` for build verification | — |
|
|
122
|
+
| NuGet restore required | Run `dotnet restore` before first `dotnet build` | 1 |
|
|
123
|
+
| Migration fails | Rollback migration (`dotnet ef migrations remove`), fix entity/config, retry | 2 |
|
|
124
|
+
| MCP scaffold output wrong | Verify against `smartstack-api.md` patterns, fix manually | — |
|
|
125
|
+
| POST-CHECK fails | Return to step-03, fix the issue, re-validate | 2 |
|
|
126
|
+
| Stuck after max retries | AskUserQuestion with options: "Try alternative", "Skip", "Discuss" | — |
|
|
127
|
+
|
|
128
|
+
**Principle:** Always fix CODE, never bypass checks. If stuck, escalate to user.
|
|
129
|
+
|
|
130
|
+
</error_handling>
|
|
131
|
+
|
|
111
132
|
<success_criteria>
|
|
112
133
|
- SmartStack context detected (context/app/module)
|
|
113
134
|
- Plan validated with skill/MCP mapped for each file
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
# SmartStack Domain API Reference
|
|
2
|
+
|
|
3
|
+
> **Source of truth:** `SmartStack.app/src/SmartStack.Domain/Common/`
|
|
4
|
+
> **Loaded by:** step-01 (analyze), step-03 (execute)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## BaseEntity
|
|
9
|
+
|
|
10
|
+
```csharp
|
|
11
|
+
namespace SmartStack.Domain.Common;
|
|
12
|
+
|
|
13
|
+
public abstract class BaseEntity
|
|
14
|
+
{
|
|
15
|
+
public Guid Id { get; set; }
|
|
16
|
+
public DateTime CreatedAt { get; set; }
|
|
17
|
+
public DateTime? UpdatedAt { get; set; }
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**ONLY 3 properties.** No Code, no IsDeleted, no RowVersion, no SoftDelete, no CreatedBy/UpdatedBy.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Interfaces
|
|
26
|
+
|
|
27
|
+
### ITenantEntity (mandatory tenant isolation)
|
|
28
|
+
|
|
29
|
+
```csharp
|
|
30
|
+
public interface ITenantEntity
|
|
31
|
+
{
|
|
32
|
+
Guid TenantId { get; }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### IAuditableEntity (audit trail)
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
public interface IAuditableEntity
|
|
40
|
+
{
|
|
41
|
+
string? CreatedBy { get; set; }
|
|
42
|
+
string? UpdatedBy { get; set; }
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### IOptionalTenantEntity (nullable tenant)
|
|
47
|
+
|
|
48
|
+
```csharp
|
|
49
|
+
public interface IOptionalTenantEntity
|
|
50
|
+
{
|
|
51
|
+
Guid? TenantId { get; }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### IScopedTenantEntity (tenant + scope visibility)
|
|
56
|
+
|
|
57
|
+
```csharp
|
|
58
|
+
public interface IScopedTenantEntity : IOptionalTenantEntity
|
|
59
|
+
{
|
|
60
|
+
EntityScope Scope { get; }
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### EntityScope enum
|
|
65
|
+
|
|
66
|
+
```csharp
|
|
67
|
+
public enum EntityScope
|
|
68
|
+
{
|
|
69
|
+
Tenant = 0, // Visible only to specific tenant (TenantId required)
|
|
70
|
+
Shared = 1, // Visible to all tenants (TenantId null)
|
|
71
|
+
Platform = 2 // Visible only to platform admins (HasGlobalAccess)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Entity Pattern (tenant-scoped, most common)
|
|
78
|
+
|
|
79
|
+
```csharp
|
|
80
|
+
using SmartStack.Domain.Common;
|
|
81
|
+
|
|
82
|
+
namespace {ProjectName}.Domain.Entities.{Context}.{App}.{Module};
|
|
83
|
+
|
|
84
|
+
public class {Name} : BaseEntity, ITenantEntity, IAuditableEntity
|
|
85
|
+
{
|
|
86
|
+
// ITenantEntity
|
|
87
|
+
public Guid TenantId { get; private set; }
|
|
88
|
+
|
|
89
|
+
// IAuditableEntity
|
|
90
|
+
public string? CreatedBy { get; set; }
|
|
91
|
+
public string? UpdatedBy { get; set; }
|
|
92
|
+
|
|
93
|
+
// Business properties (add your own)
|
|
94
|
+
public string Code { get; private set; } = null!;
|
|
95
|
+
public string Name { get; private set; } = null!;
|
|
96
|
+
public string? Description { get; private set; }
|
|
97
|
+
public bool IsActive { get; private set; } = true;
|
|
98
|
+
|
|
99
|
+
private {Name}() { }
|
|
100
|
+
|
|
101
|
+
public static {Name} Create(Guid tenantId, string code, string name)
|
|
102
|
+
{
|
|
103
|
+
if (tenantId == Guid.Empty)
|
|
104
|
+
throw new ArgumentException("TenantId is required", nameof(tenantId));
|
|
105
|
+
|
|
106
|
+
return new {Name}
|
|
107
|
+
{
|
|
108
|
+
Id = Guid.NewGuid(),
|
|
109
|
+
TenantId = tenantId,
|
|
110
|
+
Code = code.ToLowerInvariant(),
|
|
111
|
+
Name = name,
|
|
112
|
+
CreatedAt = DateTime.UtcNow
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public void Update(string name, string? description)
|
|
117
|
+
{
|
|
118
|
+
Name = name;
|
|
119
|
+
Description = description;
|
|
120
|
+
UpdatedAt = DateTime.UtcNow;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Entity Pattern (platform-level, no tenant)
|
|
128
|
+
|
|
129
|
+
```csharp
|
|
130
|
+
public class {Name} : BaseEntity, IAuditableEntity
|
|
131
|
+
{
|
|
132
|
+
public string? CreatedBy { get; set; }
|
|
133
|
+
public string? UpdatedBy { get; set; }
|
|
134
|
+
|
|
135
|
+
// Business properties
|
|
136
|
+
public string Code { get; private set; } = null!;
|
|
137
|
+
public string Name { get; private set; } = null!;
|
|
138
|
+
|
|
139
|
+
private {Name}() { }
|
|
140
|
+
|
|
141
|
+
public static {Name} Create(string code, string name)
|
|
142
|
+
{
|
|
143
|
+
return new {Name}
|
|
144
|
+
{
|
|
145
|
+
Id = Guid.NewGuid(),
|
|
146
|
+
Code = code.ToLowerInvariant(),
|
|
147
|
+
Name = name,
|
|
148
|
+
CreatedAt = DateTime.UtcNow
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## EF Configuration Pattern
|
|
157
|
+
|
|
158
|
+
```csharp
|
|
159
|
+
using Microsoft.EntityFrameworkCore;
|
|
160
|
+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
161
|
+
|
|
162
|
+
public class {Name}Configuration : IEntityTypeConfiguration<{Name}>
|
|
163
|
+
{
|
|
164
|
+
public void Configure(EntityTypeBuilder<{Name}> builder)
|
|
165
|
+
{
|
|
166
|
+
builder.ToTable("{prefix}{Name}s", "{schema}");
|
|
167
|
+
|
|
168
|
+
builder.HasKey(x => x.Id);
|
|
169
|
+
|
|
170
|
+
// Tenant (if ITenantEntity)
|
|
171
|
+
builder.Property(x => x.TenantId).IsRequired();
|
|
172
|
+
builder.HasIndex(x => x.TenantId)
|
|
173
|
+
.HasDatabaseName("IX_{prefix}{Name}s_TenantId");
|
|
174
|
+
|
|
175
|
+
// Business properties
|
|
176
|
+
builder.Property(x => x.Code).HasMaxLength(50).IsRequired();
|
|
177
|
+
builder.Property(x => x.Name).HasMaxLength(100).IsRequired();
|
|
178
|
+
builder.Property(x => x.Description).HasMaxLength(500);
|
|
179
|
+
|
|
180
|
+
// Audit (from IAuditableEntity)
|
|
181
|
+
builder.Property(x => x.CreatedBy).HasMaxLength(256);
|
|
182
|
+
builder.Property(x => x.UpdatedBy).HasMaxLength(256);
|
|
183
|
+
|
|
184
|
+
// Unique indexes
|
|
185
|
+
builder.HasIndex(x => new { x.TenantId, x.Code })
|
|
186
|
+
.IsUnique()
|
|
187
|
+
.HasDatabaseName("IX_{prefix}{Name}s_Tenant_Code");
|
|
188
|
+
|
|
189
|
+
// Relationships
|
|
190
|
+
// builder.HasMany(x => x.Children)
|
|
191
|
+
// .WithOne(x => x.Parent)
|
|
192
|
+
// .HasForeignKey(x => x.ParentId)
|
|
193
|
+
// .OnDelete(DeleteBehavior.Restrict);
|
|
194
|
+
|
|
195
|
+
// Seed data (if applicable)
|
|
196
|
+
// builder.HasData({Name}SeedData.GetSeedData());
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Service Pattern (tenant-scoped, MANDATORY)
|
|
204
|
+
|
|
205
|
+
> **CRITICAL:** ALL services MUST inject `ICurrentUser` and filter by `TenantId`. Missing TenantId = OWASP A01 vulnerability.
|
|
206
|
+
|
|
207
|
+
```csharp
|
|
208
|
+
using Microsoft.EntityFrameworkCore;
|
|
209
|
+
using Microsoft.Extensions.Logging;
|
|
210
|
+
using SmartStack.Application.Common.Interfaces.Identity;
|
|
211
|
+
using SmartStack.Application.Common.Interfaces.Persistence;
|
|
212
|
+
|
|
213
|
+
namespace {ProjectName}.Infrastructure.Services.{Context}.{App}.{Module};
|
|
214
|
+
|
|
215
|
+
public class {Name}Service : I{Name}Service
|
|
216
|
+
{
|
|
217
|
+
private readonly IExtensionsDbContext _db;
|
|
218
|
+
private readonly ICurrentUser _currentUser;
|
|
219
|
+
private readonly ILogger<{Name}Service> _logger;
|
|
220
|
+
|
|
221
|
+
public {Name}Service(
|
|
222
|
+
IExtensionsDbContext db,
|
|
223
|
+
ICurrentUser currentUser,
|
|
224
|
+
ILogger<{Name}Service> logger)
|
|
225
|
+
{
|
|
226
|
+
_db = db;
|
|
227
|
+
_currentUser = currentUser;
|
|
228
|
+
_logger = logger;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public async Task<List<{Name}ResponseDto>> GetAllAsync(CancellationToken ct)
|
|
232
|
+
{
|
|
233
|
+
return await _db.{Name}s
|
|
234
|
+
.Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY tenant filter
|
|
235
|
+
.AsNoTracking()
|
|
236
|
+
.Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
|
|
237
|
+
.ToListAsync(ct);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public async Task<{Name}ResponseDto?> GetByIdAsync(Guid id, CancellationToken ct)
|
|
241
|
+
{
|
|
242
|
+
return await _db.{Name}s
|
|
243
|
+
.Where(x => x.Id == id && x.TenantId == _currentUser.TenantId) // MANDATORY
|
|
244
|
+
.AsNoTracking()
|
|
245
|
+
.Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
|
|
246
|
+
.FirstOrDefaultAsync(ct);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
public async Task<{Name}ResponseDto> CreateAsync(Create{Name}Dto dto, CancellationToken ct)
|
|
250
|
+
{
|
|
251
|
+
var entity = {Name}.Create(
|
|
252
|
+
tenantId: _currentUser.TenantId, // MANDATORY — never Guid.Empty
|
|
253
|
+
code: dto.Code,
|
|
254
|
+
name: dto.Name);
|
|
255
|
+
|
|
256
|
+
entity.CreatedBy = _currentUser.UserId?.ToString();
|
|
257
|
+
|
|
258
|
+
_db.{Name}s.Add(entity);
|
|
259
|
+
await _db.SaveChangesAsync(ct);
|
|
260
|
+
|
|
261
|
+
_logger.LogInformation("Created {Entity} {Id} for tenant {TenantId}",
|
|
262
|
+
nameof({Name}), entity.Id, _currentUser.TenantId);
|
|
263
|
+
|
|
264
|
+
return new {Name}ResponseDto(entity.Id, entity.Code, entity.Name, entity.CreatedAt);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public async Task DeleteAsync(Guid id, CancellationToken ct)
|
|
268
|
+
{
|
|
269
|
+
var entity = await _db.{Name}s
|
|
270
|
+
.FirstOrDefaultAsync(x => x.Id == id && x.TenantId == _currentUser.TenantId, ct)
|
|
271
|
+
?? throw new KeyNotFoundException($"{Name} {id} not found");
|
|
272
|
+
|
|
273
|
+
_db.{Name}s.Remove(entity);
|
|
274
|
+
await _db.SaveChangesAsync(ct);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Key interfaces:**
|
|
280
|
+
- `ICurrentUser` (from `SmartStack.Application.Common.Interfaces.Identity`): provides `TenantId`, `UserId`, `Email`
|
|
281
|
+
- `IExtensionsDbContext` (for client extensions) or `ICoreDbContext` (for platform)
|
|
282
|
+
|
|
283
|
+
**FORBIDDEN in services:**
|
|
284
|
+
- `tenantId: Guid.Empty` — always use `_currentUser.TenantId`
|
|
285
|
+
- Queries WITHOUT `.Where(x => x.TenantId == _currentUser.TenantId)` — data leak
|
|
286
|
+
- Missing `ILogger<T>` — undiagnosable in production
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Controller Pattern (NavRoute)
|
|
291
|
+
|
|
292
|
+
```csharp
|
|
293
|
+
using Microsoft.AspNetCore.Authorization;
|
|
294
|
+
using Microsoft.AspNetCore.Mvc;
|
|
295
|
+
using SmartStack.Api.Routing;
|
|
296
|
+
using SmartStack.Api.Authorization;
|
|
297
|
+
|
|
298
|
+
namespace {ProjectName}.Api.Controllers.{Context}.{App};
|
|
299
|
+
|
|
300
|
+
[ApiController]
|
|
301
|
+
[NavRoute("{context}.{app}.{module}")]
|
|
302
|
+
[Authorize]
|
|
303
|
+
public class {Name}Controller : ControllerBase
|
|
304
|
+
{
|
|
305
|
+
private readonly I{Name}Service _service;
|
|
306
|
+
private readonly ILogger<{Name}Controller> _logger;
|
|
307
|
+
|
|
308
|
+
public {Name}Controller(I{Name}Service service, ILogger<{Name}Controller> logger)
|
|
309
|
+
{
|
|
310
|
+
_service = service;
|
|
311
|
+
_logger = logger;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
[HttpGet]
|
|
315
|
+
[RequirePermission(Permissions.{Module}.Read)]
|
|
316
|
+
public async Task<ActionResult<List<{Name}ResponseDto>>> GetAll(CancellationToken ct)
|
|
317
|
+
=> Ok(await _service.GetAllAsync(ct));
|
|
318
|
+
|
|
319
|
+
[HttpGet("{id:guid}")]
|
|
320
|
+
[RequirePermission(Permissions.{Module}.Read)]
|
|
321
|
+
public async Task<ActionResult<{Name}ResponseDto>> GetById(Guid id, CancellationToken ct)
|
|
322
|
+
{
|
|
323
|
+
var result = await _service.GetByIdAsync(id, ct);
|
|
324
|
+
return result is null ? NotFound() : Ok(result);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
[HttpPost]
|
|
328
|
+
[RequirePermission(Permissions.{Module}.Create)]
|
|
329
|
+
public async Task<ActionResult<{Name}ResponseDto>> Create([FromBody] Create{Name}Dto dto, CancellationToken ct)
|
|
330
|
+
{
|
|
331
|
+
var result = await _service.CreateAsync(dto, ct);
|
|
332
|
+
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
[HttpDelete("{id:guid}")]
|
|
336
|
+
[RequirePermission(Permissions.{Module}.Delete)]
|
|
337
|
+
public async Task<ActionResult> Delete(Guid id, CancellationToken ct)
|
|
338
|
+
{
|
|
339
|
+
await _service.DeleteAsync(id, ct);
|
|
340
|
+
return NoContent();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**CRITICAL:** Use `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint — NEVER `[Authorize]` alone (no RBAC enforcement).
|
|
346
|
+
|
|
347
|
+
**Namespace:** `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)
|
|
348
|
+
|
|
349
|
+
**NavRoute resolves at startup from DB:** `platform.administration.users` → `api/platform/administration/users`
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Navigation Seed Data Pattern (CRITICAL — routes must be full paths)
|
|
354
|
+
|
|
355
|
+
> **The navigation seed data defines menu routes stored in DB. These routes MUST be full paths starting with `/`.**
|
|
356
|
+
> Short routes (e.g., `humanresources`) cause 400 Bad Request on application-tracking.
|
|
357
|
+
|
|
358
|
+
### Route Convention
|
|
359
|
+
|
|
360
|
+
| Level | Route Format | Example |
|
|
361
|
+
|-------|-------------|---------|
|
|
362
|
+
| Application | `/{context}/{app-kebab}` | `/business/human-resources` |
|
|
363
|
+
| Module | `/{context}/{app-kebab}/{module-kebab}` | `/business/human-resources/employees` |
|
|
364
|
+
| Section | `/{context}/{app-kebab}/{module-kebab}/{section-kebab}` | `/business/human-resources/employees/list` |
|
|
365
|
+
| Resource | `/{context}/{app-kebab}/{module-kebab}/{section-kebab}/{resource-kebab}` | `/business/human-resources/employees/list/export` |
|
|
366
|
+
|
|
367
|
+
**Rules:**
|
|
368
|
+
- Routes ALWAYS start with `/`
|
|
369
|
+
- Routes ALWAYS include the full hierarchy from context to current level
|
|
370
|
+
- Routes ALWAYS use kebab-case (NOT PascalCase, NOT camelCase)
|
|
371
|
+
- Code identifiers stay PascalCase in C# (`HumanResources`) but routes are kebab-case (`human-resources`)
|
|
372
|
+
|
|
373
|
+
### ToKebabCase Helper (include in SeedConstants or SeedDataProvider)
|
|
374
|
+
|
|
375
|
+
```csharp
|
|
376
|
+
private static string ToKebabCase(string value)
|
|
377
|
+
=> System.Text.RegularExpressions.Regex
|
|
378
|
+
.Replace(value, "(?<!^)([A-Z])", "-$1")
|
|
379
|
+
.ToLowerInvariant();
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### SeedConstants Pattern
|
|
383
|
+
|
|
384
|
+
```csharp
|
|
385
|
+
public static class SeedConstants
|
|
386
|
+
{
|
|
387
|
+
// Deterministic GUIDs (SHA256-based, reproducible across environments)
|
|
388
|
+
public static readonly Guid ApplicationId = DeterministicGuid("nav:business.humanresources");
|
|
389
|
+
public static readonly Guid ModuleId = DeterministicGuid("nav:business.humanresources.employees");
|
|
390
|
+
public static readonly Guid SectionId = DeterministicGuid("nav:business.humanresources.employees.list");
|
|
391
|
+
|
|
392
|
+
private static Guid DeterministicGuid(string input)
|
|
393
|
+
{
|
|
394
|
+
var hash = System.Security.Cryptography.SHA256.HashData(
|
|
395
|
+
System.Text.Encoding.UTF8.GetBytes(input));
|
|
396
|
+
var bytes = new byte[16];
|
|
397
|
+
Array.Copy(hash, bytes, 16);
|
|
398
|
+
bytes[6] = (byte)((bytes[6] & 0x0F) | 0x50); // version 5
|
|
399
|
+
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80); // variant
|
|
400
|
+
return new Guid(bytes);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Navigation Seed Data Example
|
|
406
|
+
|
|
407
|
+
```csharp
|
|
408
|
+
// Application: /business/human-resources
|
|
409
|
+
var app = NavigationApplication.Create(
|
|
410
|
+
businessCtx.Id, "humanresources", "Human Resources", "HR Management",
|
|
411
|
+
"Users", IconType.Lucide,
|
|
412
|
+
"/business/human-resources", // FULL PATH — starts with /, kebab-case
|
|
413
|
+
10);
|
|
414
|
+
|
|
415
|
+
// Module: /business/human-resources/employees
|
|
416
|
+
var module = NavigationModule.Create(
|
|
417
|
+
app.Id, "employees", "Employees", "Employee management",
|
|
418
|
+
"UserCheck", IconType.Lucide,
|
|
419
|
+
"/business/human-resources/employees", // FULL PATH — includes parent
|
|
420
|
+
10);
|
|
421
|
+
|
|
422
|
+
// Section: /business/human-resources/employees/departments
|
|
423
|
+
var section = NavigationSection.Create(
|
|
424
|
+
module.Id, "departments", "Departments", "Manage departments",
|
|
425
|
+
"Building2", IconType.Lucide,
|
|
426
|
+
"/business/human-resources/employees/departments", // FULL PATH
|
|
427
|
+
10);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### FORBIDDEN in Seed Data
|
|
431
|
+
|
|
432
|
+
| Mistake | Reality |
|
|
433
|
+
|---------|---------|
|
|
434
|
+
| `"humanresources"` as route | Must be `"/business/human-resources"` (full path, kebab-case) |
|
|
435
|
+
| `"employees"` as route | Must be `"/business/human-resources/employees"` (includes parent) |
|
|
436
|
+
| `Guid.NewGuid()` in seed data | Must use deterministic GUIDs (SHA256) |
|
|
437
|
+
| Missing translations | Must have 4 languages: fr, en, it, de |
|
|
438
|
+
| Missing NavigationApplicationSeedData | Menu invisible without Application level |
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## DbContext Pattern (extensions)
|
|
443
|
+
|
|
444
|
+
```csharp
|
|
445
|
+
// In IExtensionsDbContext.cs:
|
|
446
|
+
public DbSet<{Name}> {Name}s => Set<{Name}>();
|
|
447
|
+
|
|
448
|
+
// In ExtensionsDbContext.cs (same line):
|
|
449
|
+
public DbSet<{Name}> {Name}s => Set<{Name}>();
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## DI Registration Pattern
|
|
455
|
+
|
|
456
|
+
```csharp
|
|
457
|
+
// In DependencyInjection.cs or ServiceCollectionExtensions.cs:
|
|
458
|
+
services.AddScoped<I{Name}Service, {Name}Service>();
|
|
459
|
+
services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Common Mistakes to Avoid
|
|
465
|
+
|
|
466
|
+
| Mistake | Reality |
|
|
467
|
+
|---------|---------|
|
|
468
|
+
| `entity.SoftDelete()` | Does NOT exist — no soft delete in BaseEntity |
|
|
469
|
+
| `entity.Code` inherited | Code is a business property — add it yourself |
|
|
470
|
+
| `e.RowVersion` in config | Does NOT exist in BaseEntity |
|
|
471
|
+
| `e.IsDeleted` filter | Does NOT exist — no soft delete |
|
|
472
|
+
| `SmartStack.Api.Core.Routing` | Wrong — use `SmartStack.Api.Routing` |
|
|
473
|
+
| `SystemEntity` base class | Does NOT exist — use `BaseEntity` for all |
|
|
474
|
+
| `[Route] + [NavRoute]` | Only `[NavRoute]` needed (resolves route from DB) |
|
|
475
|
+
| `SmartStack.Domain.Common.Interfaces` | Wrong — interfaces are in `SmartStack.Domain.Common` directly |
|
|
476
|
+
| `[Authorize]` without `[RequirePermission]` | No RBAC enforcement — always use `[RequirePermission]` |
|
|
477
|
+
| `tenantId: Guid.Empty` in services | OWASP A01 — always use `_currentUser.TenantId` |
|
|
478
|
+
| Service without `ICurrentUser` | All tenant data leaks — inject `ICurrentUser` |
|
|
479
|
+
| Route `"humanresources"` in seed data | Must be full path `"/business/human-resources"` |
|
|
480
|
+
| Route without leading `/` | All routes must start with `/` |
|
|
481
|
+
| `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
| Validate | MCP `validate_conventions` |
|
|
18
18
|
|
|
19
19
|
**Rules:**
|
|
20
|
-
- Inherit `
|
|
21
|
-
-
|
|
20
|
+
- Inherit `BaseEntity`, implement `ITenantEntity` + `IAuditableEntity`
|
|
21
|
+
- See `references/smartstack-api.md` for exact BaseEntity API (Id, CreatedAt, UpdatedAt only)
|
|
22
|
+
- No Code/IsDeleted/RowVersion in BaseEntity — add business properties yourself
|
|
22
23
|
- Domain events for state changes
|
|
23
24
|
- Value objects for composite values
|
|
24
25
|
|
|
@@ -49,7 +50,8 @@
|
|
|
49
50
|
|
|
50
51
|
## Layer 1 — Application (parallel with API)
|
|
51
52
|
|
|
52
|
-
**Services:** `Application/Services/{ContextPascal}/{App}/{Module}/`
|
|
53
|
+
**Services:** `Application/Services/{ContextPascal}/{App}/{Module}/` (interface)
|
|
54
|
+
**Service impls:** `Infrastructure/Services/{ContextPascal}/{App}/{Module}/` (implementation)
|
|
53
55
|
**DTOs:** `Application/DTOs/{ContextPascal}/{App}/{Module}/`
|
|
54
56
|
**Validators:** `Application/Validators/{ContextPascal}/{App}/{Module}/`
|
|
55
57
|
|
|
@@ -60,10 +62,13 @@
|
|
|
60
62
|
| Validate | MCP `validate_conventions` |
|
|
61
63
|
|
|
62
64
|
**Rules:**
|
|
65
|
+
- **ALL services MUST inject `ICurrentUser` and filter by `TenantId`** (see `smartstack-api.md` Service Pattern)
|
|
66
|
+
- **ALL services MUST inject `ILogger<T>`** for production diagnostics
|
|
63
67
|
- CQRS with MediatR
|
|
64
68
|
- FluentValidation for all commands
|
|
65
69
|
- DTOs separate from domain entities
|
|
66
70
|
- Service interfaces in Application, implementations in Infrastructure
|
|
71
|
+
- **FORBIDDEN:** `tenantId: Guid.Empty`, queries without TenantId filter, services without ICurrentUser
|
|
67
72
|
|
|
68
73
|
---
|
|
69
74
|
|
|
@@ -96,27 +101,92 @@
|
|
|
96
101
|
|
|
97
102
|
**Folder:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/`
|
|
98
103
|
|
|
104
|
+
> **Detailed templates:** See ralph-loop `references/core-seed-data.md` for complete C# code templates.
|
|
105
|
+
> Navigation hierarchy: Context → Application → Module → Section → Resource (ALL levels need seed data).
|
|
106
|
+
|
|
99
107
|
| Action | Tool |
|
|
100
108
|
|--------|------|
|
|
101
109
|
| Generate permissions | MCP `generate_permissions` (PRIMARY) |
|
|
102
|
-
| Navigation seed |
|
|
103
|
-
| Roles seed |
|
|
104
|
-
| Provider |
|
|
105
|
-
|
|
106
|
-
### Seed Data Chain (
|
|
107
|
-
|
|
108
|
-
1. **
|
|
109
|
-
2. **
|
|
110
|
-
3. **
|
|
111
|
-
4. **
|
|
112
|
-
5. **
|
|
113
|
-
|
|
110
|
+
| Navigation seed | Templates below |
|
|
111
|
+
| Roles seed | Templates below |
|
|
112
|
+
| Provider | Templates below |
|
|
113
|
+
|
|
114
|
+
### Seed Data Chain (7 files minimum)
|
|
115
|
+
|
|
116
|
+
1. **NavigationApplicationSeedData.cs** — Application-level navigation entry (MUST be first)
|
|
117
|
+
2. **NavigationModuleSeedData.cs** — Deterministic GUIDs (SHA256), 4 languages (fr, en, it, de)
|
|
118
|
+
3. **NavigationSectionSeedData.cs** — Section-level navigation (if sections defined)
|
|
119
|
+
4. **NavigationResourceSeedData.cs** — Resource-level navigation (if resources defined)
|
|
120
|
+
5. **PermissionsSeedData.cs** — MCP `generate_permissions` first, fallback template
|
|
121
|
+
6. **RolesSeedData.cs** — Context-based: Admin=CRUD, Manager=CRU, Contributor=CR, Viewer=R
|
|
122
|
+
7. **{App}SeedDataProvider.cs** — Implements IClientSeedDataProvider
|
|
123
|
+
- `SeedNavigationAsync()` — seeds Application → Module → Section → Resource + translations
|
|
124
|
+
- `SeedPermissionsAsync()` + `SeedRolePermissionsAsync()`
|
|
114
125
|
- DI: `services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()`
|
|
115
126
|
|
|
127
|
+
### Deterministic GUID Pattern
|
|
128
|
+
|
|
129
|
+
```csharp
|
|
130
|
+
// Use SHA256 for deterministic GUIDs (reproducible across environments)
|
|
131
|
+
public static readonly Guid ModuleId = GenerateDeterministicGuid("nav-module-{app}-{module}");
|
|
132
|
+
|
|
133
|
+
private static Guid GenerateDeterministicGuid(string input)
|
|
134
|
+
{
|
|
135
|
+
var hash = System.Security.Cryptography.SHA256.HashData(
|
|
136
|
+
System.Text.Encoding.UTF8.GetBytes(input));
|
|
137
|
+
var bytes = new byte[16];
|
|
138
|
+
Array.Copy(hash, bytes, 16);
|
|
139
|
+
return new Guid(bytes);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Route Convention (CRITICAL — Full Paths Required)
|
|
144
|
+
|
|
145
|
+
> **Routes stored in DB drive the platform menu AND application-tracking.**
|
|
146
|
+
> Short routes (e.g., `humanresources`) cause **400 Bad Request** on every page navigation.
|
|
147
|
+
|
|
148
|
+
**Route format: `/{context}/{app-kebab}/{module-kebab}/{section-kebab}`**
|
|
149
|
+
|
|
150
|
+
| Level | Code (C#) | Route (DB) |
|
|
151
|
+
|-------|-----------|-----------|
|
|
152
|
+
| Application | `humanresources` | `/business/human-resources` |
|
|
153
|
+
| Module | `employees` | `/business/human-resources/employees` |
|
|
154
|
+
| Section | `departments` | `/business/human-resources/employees/departments` |
|
|
155
|
+
| Resource | `export` | `/business/human-resources/employees/departments/export` |
|
|
156
|
+
|
|
157
|
+
**Platform examples (verified from SmartStack.app):**
|
|
158
|
+
- `/platform/administration` (not `administration`)
|
|
159
|
+
- `/platform/administration/users` (not `users`)
|
|
160
|
+
- `/personal/myspace/profile` (not `profile`)
|
|
161
|
+
|
|
162
|
+
- PascalCase for C# code identifiers (`HumanResources`)
|
|
163
|
+
- **kebab-case** for ALL URL routes in seed data (`human-resources`)
|
|
164
|
+
- **Routes MUST start with `/`** and include full parent hierarchy
|
|
165
|
+
- Helper: `ToKebabCase()` transforms PascalCase → kebab-case
|
|
166
|
+
|
|
167
|
+
```csharp
|
|
168
|
+
private static string ToKebabCase(string value)
|
|
169
|
+
=> System.Text.RegularExpressions.Regex.Replace(value, "(?<!^)([A-Z])", "-$1").ToLowerInvariant();
|
|
170
|
+
|
|
171
|
+
// Route construction:
|
|
172
|
+
var appRoute = $"/{ToKebabCase(contextCode)}/{ToKebabCase(appCode)}";
|
|
173
|
+
// → "/business/human-resources"
|
|
174
|
+
|
|
175
|
+
var moduleRoute = $"{appRoute}/{ToKebabCase(moduleCode)}";
|
|
176
|
+
// → "/business/human-resources/employees"
|
|
177
|
+
|
|
178
|
+
var sectionRoute = $"{moduleRoute}/{ToKebabCase(sectionCode)}";
|
|
179
|
+
// → "/business/human-resources/employees/departments"
|
|
180
|
+
```
|
|
181
|
+
|
|
116
182
|
**FORBIDDEN:**
|
|
117
183
|
- `Guid.NewGuid()` → use deterministic GUIDs (SHA256)
|
|
118
184
|
- Missing translations (must have fr, en, it, de)
|
|
119
185
|
- Empty seed classes with no seeding logic
|
|
186
|
+
- PascalCase in route URLs → always kebab-case
|
|
187
|
+
- Missing NavigationApplicationSeedData → menu invisible
|
|
188
|
+
- **Short routes without `/` prefix** → `"humanresources"` must be `"/business/human-resources"`
|
|
189
|
+
- **Routes without parent hierarchy** → `"employees"` must be `"/business/human-resources/employees"`
|
|
120
190
|
|
|
121
191
|
---
|
|
122
192
|
|