@atlashub/smartstack-cli 4.75.0 → 4.79.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 +87 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/project/claude-md/root.CLAUDE.md.template +1 -1
- package/templates/skills/ai-prompt/SKILL.md +64 -0
- package/templates/skills/ai-prompt/references/ai-agent-modes.md +89 -0
- package/templates/skills/ai-prompt/references/eval-framework.md +129 -0
- package/templates/skills/apex/SKILL.md +2 -2
- package/templates/skills/apex/references/checks/frontend-checks.sh +123 -11
- package/templates/skills/apex/references/checks/seed-checks.sh +81 -7
- package/templates/skills/apex/references/core-seed-data.md +27 -22
- package/templates/skills/apex/references/domain-events-pattern.md +45 -0
- package/templates/skills/apex/references/entity-hooks-pattern.md +68 -0
- package/templates/skills/apex/references/licensing-enforcement.md +52 -0
- package/templates/skills/apex/references/post-checks.md +18 -1
- package/templates/skills/apex/references/smartstack-api.md +116 -5
- package/templates/skills/apex/references/smartstack-frontend.md +1 -1
- package/templates/skills/apex/references/smartstack-layers.md +6 -6
- package/templates/skills/apex/steps/step-00-init.md +1 -1
- package/templates/skills/apex/steps/step-03b-layer1-seed.md +26 -0
- package/templates/skills/apex/steps/step-03d-layer3-frontend.md +124 -2
- package/templates/skills/apex/steps/step-04-examine.md +163 -0
- package/templates/skills/apex-verify/SKILL.md +110 -0
- package/templates/skills/apex-verify/references/audit-rules.md +50 -0
- package/templates/skills/apex-verify/steps/step-00-init.md +119 -0
- package/templates/skills/apex-verify/steps/step-01-nav-audit.md +96 -0
- package/templates/skills/apex-verify/steps/step-02-crud-audit.md +127 -0
- package/templates/skills/apex-verify/steps/step-03-perm-audit.md +119 -0
- package/templates/skills/apex-verify/steps/step-04-route-audit.md +98 -0
- package/templates/skills/apex-verify/steps/step-05-report.md +110 -0
- package/templates/skills/application/references/contexts-cheatsheet.md +86 -0
- package/templates/skills/application/references/extensions-system.md +158 -0
- package/templates/skills/application/references/frontend-route-naming.md +7 -5
- package/templates/skills/application/references/frontend-verification.md +7 -5
- package/templates/skills/application/references/provider-template.md +4 -2
- package/templates/skills/application/references/smartstack-provider.md +118 -0
- package/templates/skills/application/references/themes-db-driven.md +484 -0
- package/templates/skills/application/templates-frontend.md +2 -2
- package/templates/skills/application/templates-seed.md +4 -2
- package/templates/skills/audit-route/references/routing-pattern.md +3 -1
- package/templates/skills/business-analyse/SKILL.md +3 -3
- package/templates/skills/business-analyse/_shared.md +37 -0
- package/templates/skills/business-analyse/react/components.md +30 -28
- package/templates/skills/business-analyse/references/03-json-schemas.md +11 -3
- package/templates/skills/business-analyse/references/03-post-check-validation.md +64 -0
- package/templates/skills/business-analyse/references/canonical-json-formats.md +7 -3
- package/templates/skills/business-analyse/references/robustness-checks.md +1 -1
- package/templates/skills/business-analyse/references/validation-checklist.md +5 -5
- package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +15 -4
- package/templates/skills/business-analyse/steps/step-03-specify.md +162 -4
- package/templates/skills/business-analyse/steps/step-04-consolidate.md +211 -1
- package/templates/skills/business-analyse/templates-react.md +15 -15
- package/templates/skills/business-analyse-handoff/references/agent-handoff-transform-prompt.md +3 -0
- package/templates/skills/business-analyse-html/html/ba-interactive.html +198 -16
- package/templates/skills/business-analyse-html/html/src/scripts/01-data-init.js +64 -0
- package/templates/skills/business-analyse-html/html/src/scripts/05-render-specs.js +80 -11
- package/templates/skills/business-analyse-html/html/src/scripts/06-render-consolidation.js +2 -2
- package/templates/skills/business-analyse-html/html/src/scripts/06-render-mockups.js +6 -3
- package/templates/skills/business-analyse-html/html/src/scripts/12-render-diagrams.js +46 -0
- package/templates/skills/business-analyse-html/references/02-feature-data-building.md +4 -2
- package/templates/skills/business-analyse-html/references/data-build.md +2 -0
- package/templates/skills/business-analyse-html/references/data-mapping.md +88 -21
- package/templates/skills/business-analyse-html/steps/step-02-build-data.md +6 -0
- package/templates/skills/business-analyse-html/steps/step-04-verify.md +92 -3
- package/templates/skills/business-analyse-quick/SKILL.md +807 -0
- package/templates/skills/{sketch → business-analyse-quick}/references/domain-heuristics.md +59 -3
- package/templates/skills/business-analyse-quick/references/prd-schema.md +268 -0
- package/templates/skills/business-analyse-review/references/review-data-mapping.md +6 -0
- package/templates/skills/cli-app-sync/SKILL.md +105 -4
- package/templates/skills/cli-app-sync/references/comparison-map.md +13 -0
- package/templates/skills/cli-app-sync/references/diff-entities.md +162 -0
- package/templates/skills/dev-start/SKILL.md +7 -7
- package/templates/skills/documentation/templates.md +16 -16
- package/templates/skills/migrate/SKILL.md +312 -0
- package/templates/skills/migrate/references/v3.34-to-v3.46.md +289 -0
- package/templates/skills/sketch/SKILL.md +15 -153
- package/templates/skills/smoke-generation/SKILL.md +313 -0
- package/templates/skills/ui-components/SKILL.md +11 -1
- package/templates/skills/ui-components/patterns/data-table.md +1 -1
- package/templates/skills/ui-components/references/component-catalog.md +82 -0
- package/templates/skills/workflow/SKILL.md +70 -1
|
@@ -109,7 +109,6 @@ public static class NavigationApplicationSeedData
|
|
|
109
109
|
return new NavigationApplicationSeedEntry
|
|
110
110
|
{
|
|
111
111
|
Id = ApplicationId,
|
|
112
|
-
Zone = ApplicationZone.Business,
|
|
113
112
|
Code = "{appCode}",
|
|
114
113
|
Label = "{appLabel_en}",
|
|
115
114
|
Description = "{appDesc_en}",
|
|
@@ -117,7 +116,9 @@ public static class NavigationApplicationSeedData
|
|
|
117
116
|
IconType = IconType.Lucide,
|
|
118
117
|
Route = ToKebabCase("/{appCode}"),
|
|
119
118
|
DisplayOrder = 1,
|
|
120
|
-
IsActive = true
|
|
119
|
+
IsActive = true,
|
|
120
|
+
IsOpen = false, // true => bypass permission checks (system apps only)
|
|
121
|
+
IsPersonal = false // true => belongs to user personal scope (myspace)
|
|
121
122
|
};
|
|
122
123
|
}
|
|
123
124
|
|
|
@@ -199,7 +200,6 @@ public static class NavigationApplicationSeedData
|
|
|
199
200
|
public class NavigationApplicationSeedEntry
|
|
200
201
|
{
|
|
201
202
|
public Guid Id { get; init; }
|
|
202
|
-
public ApplicationZone Zone { get; init; }
|
|
203
203
|
public string Code { get; init; } = null!;
|
|
204
204
|
public string Label { get; init; } = null!;
|
|
205
205
|
public string? Description { get; init; }
|
|
@@ -208,6 +208,9 @@ public class NavigationApplicationSeedEntry
|
|
|
208
208
|
public string? Route { get; init; }
|
|
209
209
|
public int DisplayOrder { get; init; }
|
|
210
210
|
public bool IsActive { get; init; }
|
|
211
|
+
// v3.46+ : ApplicationZone enum removed. Replaced by 2 boolean flags.
|
|
212
|
+
public bool IsOpen { get; init; } // true => app accessible without permission checks
|
|
213
|
+
public bool IsPersonal { get; init; } // true => app in user personal scope (myspace)
|
|
211
214
|
}
|
|
212
215
|
```
|
|
213
216
|
|
|
@@ -404,12 +407,11 @@ public static readonly Guid {ResourcePascal}ResourceId = Guid.NewGuid();
|
|
|
404
407
|
|
|
405
408
|
### Section Methods (add to {ModulePascal}NavigationSeedData.cs)
|
|
406
409
|
|
|
407
|
-
> **ROUTE
|
|
408
|
-
>
|
|
409
|
-
> - `list` section route = module route (e.g., `/human-resources/employees`)
|
|
410
|
-
> - `detail` section route = module route + `/:id` (e.g., `/human-resources/employees/:id`) — NOT `/detail/:id`
|
|
411
|
-
> -
|
|
412
|
-
> - Other sections (dashboard, approve, import) = module route + `/{section-kebab}` (normal)
|
|
410
|
+
> **ROUTE CONVENTION:**
|
|
411
|
+
> ALL sections use a UNIFORM route pattern: module route + `/{section-code}`.
|
|
412
|
+
> - `list` section route = module route + `/list` (e.g., `/human-resources/employees/list`)
|
|
413
|
+
> - `detail` section route = module route + `/:id` (e.g., `/human-resources/employees/:id`) — NOT `/detail/:id` (detail is an implicit route, not a section)
|
|
414
|
+
> - Other sections (dashboard, approve, import) = module route + `/{section-kebab}` (same rule)
|
|
413
415
|
|
|
414
416
|
```csharp
|
|
415
417
|
// --- Add AFTER GetTranslationEntries() in {ModulePascal}NavigationSeedData.cs ---
|
|
@@ -434,14 +436,18 @@ public static IEnumerable<NavigationSectionSeedEntry> GetSectionEntries(Guid mod
|
|
|
434
436
|
Description = "{section1_desc_en}",
|
|
435
437
|
Icon = "{section1_icon}",
|
|
436
438
|
IconType = IconType.Lucide,
|
|
437
|
-
// ROUTE CONVENTION:
|
|
438
|
-
// - "list" section →
|
|
439
|
-
// -
|
|
440
|
-
//
|
|
441
|
-
// Do not use: "/employees/list", "/employees/detail/:id"
|
|
439
|
+
// ROUTE CONVENTION: ALL sections use module route + "/{section-code}" (uniform rule)
|
|
440
|
+
// - "list" section → module route + "/list" (e.g., /human-resources/employees/list)
|
|
441
|
+
// - Other sections → module route + "/{section-kebab}" (same rule)
|
|
442
|
+
// Implicit routes (detail, create, edit) are NOT sections — registered by DynamicRouter convention
|
|
442
443
|
Route = "{section_route}", // From seedDataCore.navigationSections[].route
|
|
443
444
|
DisplayOrder = {section1_sort},
|
|
444
|
-
|
|
445
|
+
// VISIBILITY RULE (MANDATORY):
|
|
446
|
+
// Reserved codes ("detail", "create", "edit") and codes containing "-detail"
|
|
447
|
+
// are internal route targets, NOT sidebar menu items.
|
|
448
|
+
// → IsActive = false for these codes (DynamicRouter resolves them by convention)
|
|
449
|
+
// → IsActive = true for all other sections (visible in sidebar menu)
|
|
450
|
+
IsActive = true // Set to FALSE if section code is "detail", "create", "edit", or "*-detail"
|
|
445
451
|
}
|
|
446
452
|
// Repeat for each section...
|
|
447
453
|
};
|
|
@@ -522,15 +528,14 @@ public static IEnumerable<NavigationResourceSeedEntry> GetResourceEntries(Guid s
|
|
|
522
528
|
{
|
|
523
529
|
// RESOURCE ROUTE CONVENTION:
|
|
524
530
|
// Resources inherit their parent section's resolved route as base:
|
|
525
|
-
// - Under "list" section → base = module route
|
|
526
|
-
// - Under "detail" section → base = module route (no /detail, resource routes don't include /:id)
|
|
531
|
+
// - Under "list" section → base = module route + /list
|
|
527
532
|
// - Under other sections → base = module route + /{section-kebab}
|
|
528
533
|
// Then append: /{resource-kebab}
|
|
529
534
|
//
|
|
530
535
|
// Example: resource "export" under section "dashboard":
|
|
531
536
|
// Route = /human-resources/employees/dashboard/export
|
|
532
537
|
// Example: resource "employees-grid" under section "list":
|
|
533
|
-
// Route = /human-resources/employees/
|
|
538
|
+
// Route = /human-resources/employees/list/employees-grid
|
|
534
539
|
new NavigationResourceSeedEntry
|
|
535
540
|
{
|
|
536
541
|
Id = {Resource1Pascal}ResourceId,
|
|
@@ -539,7 +544,7 @@ public static IEnumerable<NavigationResourceSeedEntry> GetResourceEntries(Guid s
|
|
|
539
544
|
Label = "{resource1_label_en}",
|
|
540
545
|
EntityType = "{resource1_entity}",
|
|
541
546
|
// Use parent section's resolved route + /{resource-kebab}
|
|
542
|
-
// For "list"
|
|
547
|
+
// For "list" sections, route = module route + /list. "detail" is implicit (not a section).
|
|
543
548
|
Route = "{resource_route}", // From seedDataCore: parent section route + /{resource-kebab}
|
|
544
549
|
DisplayOrder = 1
|
|
545
550
|
}
|
|
@@ -591,10 +596,10 @@ public class NavigationResourceSeedEntry
|
|
|
591
596
|
| `{section_label_xx}` | `specification.navigation.entries[]` where `level == "section"` → `labels.xx` |
|
|
592
597
|
| `{section_icon}` | `seedDataCore.navigationSections[].icon` |
|
|
593
598
|
| `{section_sort}` | `seedDataCore.navigationSections[].sort` |
|
|
594
|
-
| `{section_route}` | `seedDataCore.navigationSections[].route` —
|
|
599
|
+
| `{section_route}` | `seedDataCore.navigationSections[].route` — ALL sections use uniform rule: module route + `/{section-code}` (`list` → module route + `/list`). `detail` is implicit (not a section): module route + `/:id`. |
|
|
595
600
|
| `{resourceCode}` | `seedDataCore.navigationResources[].code` |
|
|
596
601
|
| `{resource_entity}` | `seedDataCore.navigationResources[].entity` |
|
|
597
|
-
| `{resource_route}` | Computed from parent section route + `/{resource-kebab}`.
|
|
602
|
+
| `{resource_route}` | Computed from parent section route + `/{resource-kebab}`. For `list` parent → module route + `/list/{resource-kebab}`. For other parents → module route + `/{section-kebab}/{resource-kebab}`. |
|
|
598
603
|
| `{parentSectionCode}` | `seedDataCore.navigationResources[].parentCode` |
|
|
599
604
|
|
|
600
605
|
---
|
|
@@ -1367,7 +1372,7 @@ Before marking the task as completed, verify ALL:
|
|
|
1367
1372
|
**Application-Level (FIRST — before modules):**
|
|
1368
1373
|
- [ ] `NavigationApplicationSeedData.cs` created (once per application, at `Infrastructure/Persistence/Seeding/Data/`)
|
|
1369
1374
|
- [ ] Application GUID is random (`Guid.NewGuid()`) — FK resolution is by Code lookup, not fixed ID
|
|
1370
|
-
- [ ] GetApplicationEntry() takes no parameters, includes `
|
|
1375
|
+
- [ ] GetApplicationEntry() takes no parameters, includes `IsOpen = false` and `IsPersonal = false` (set `IsPersonal = true` only for myspace-scoped apps; set `IsOpen = true` only for public/system apps that should bypass permission checks). v3.46+ : `ApplicationZone` enum is removed — do NOT add `Zone = ...`
|
|
1371
1376
|
- [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application), using `app.Id` (actual DB ID) for EntityId
|
|
1372
1377
|
- [ ] `IClientSeedDataProvider.SeedNavigationAsync()` uses `NavigationApplicationSeedData` (NO hardcoded `{appLabel_en}` / `{appIcon}` placeholders)
|
|
1373
1378
|
- [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (DTO only — provider code resolves from DB by Code)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Domain Events (lightweight pub-sub)
|
|
2
|
+
|
|
3
|
+
> Extracted from `smartstack-api.md` for clarity. Loaded only when generating code that needs events.
|
|
4
|
+
|
|
5
|
+
SmartStack uses a minimal domain event pattern in `SmartStack.Domain.Support.Events`.
|
|
6
|
+
|
|
7
|
+
## Interface + base class
|
|
8
|
+
|
|
9
|
+
```csharp
|
|
10
|
+
public interface IDomainEvent
|
|
11
|
+
{
|
|
12
|
+
DateTime OccurredAt { get; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public abstract class DomainEvent : IDomainEvent
|
|
16
|
+
{
|
|
17
|
+
public DateTime OccurredAt { get; } = DateTime.UtcNow;
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## When to raise an event vs use a hook
|
|
22
|
+
|
|
23
|
+
| Use case | Mechanism |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Cross-cutting side effect tied to entity lifecycle (Create/Update/Delete) | Entity hook (`IAfterCreate<T>`, …) — see [entity-hooks-pattern.md](entity-hooks-pattern.md) |
|
|
26
|
+
| Business event independent of CRUD (e.g. `WorkflowExecuted`, `LicenseRenewed`) | Domain event |
|
|
27
|
+
| Need to handle multiple unrelated reactions to the same business fact | Domain event |
|
|
28
|
+
| Reaction must be transactional with the entity write | Neither — code it inline in the handler |
|
|
29
|
+
|
|
30
|
+
## Convention
|
|
31
|
+
|
|
32
|
+
- One event class per business fact, named `{PastTense}Event` (e.g. `EmployeeOnboardedEvent`, `WorkflowExecutedEvent`).
|
|
33
|
+
- Place events in `Domain/{Aggregate}/Events/`.
|
|
34
|
+
- Inherit `DomainEvent` (gets `OccurredAt` for free).
|
|
35
|
+
- Dispatched by handlers (no automatic publication from `SaveChangesAsync` in v3.46) — keep dispatch explicit so the handler controls timing.
|
|
36
|
+
|
|
37
|
+
## Example dispatch (handler)
|
|
38
|
+
|
|
39
|
+
```csharp
|
|
40
|
+
await _mediator.Publish(new EmployeeOnboardedEvent(employee.Id, employee.TenantId), ct);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Source
|
|
44
|
+
|
|
45
|
+
- `D:/01 - projets/SmartStack.app/features/IA-Workflow/src/SmartStack.Domain/Support/Events/IDomainEvent.cs`
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Entity Lifecycle Hooks (v3.46+)
|
|
2
|
+
|
|
3
|
+
> Extracted from `smartstack-api.md` for clarity. Loaded only when generating code that needs hooks.
|
|
4
|
+
|
|
5
|
+
SmartStack exposes 6 typed hook interfaces + 1 executor in `SmartStack.Application.Common.Interfaces.Hooks`.
|
|
6
|
+
|
|
7
|
+
## Interfaces
|
|
8
|
+
|
|
9
|
+
```csharp
|
|
10
|
+
public interface IBeforeCreate<in T> where T : class { int Order => 0; Task ExecuteAsync(T entity, CancellationToken ct = default); }
|
|
11
|
+
public interface IAfterCreate<in T> where T : class { int Order => 0; Task ExecuteAsync(T entity, CancellationToken ct = default); }
|
|
12
|
+
public interface IBeforeUpdate<in T> where T : class { int Order => 0; Task ExecuteAsync(T entity, CancellationToken ct = default); }
|
|
13
|
+
public interface IAfterUpdate<in T> where T : class { int Order => 0; Task ExecuteAsync(T entity, CancellationToken ct = default); }
|
|
14
|
+
public interface IBeforeDelete<in T> where T : class { int Order => 0; Task ExecuteAsync(T entity, CancellationToken ct = default); }
|
|
15
|
+
public interface IAfterDelete<in T> where T : class { int Order => 0; Task ExecuteAsync(T entity, CancellationToken ct = default); }
|
|
16
|
+
|
|
17
|
+
public interface IHookExecutor
|
|
18
|
+
{
|
|
19
|
+
Task ExecuteBeforeCreateAsync<T>(T entity, CancellationToken ct = default) where T : class;
|
|
20
|
+
Task ExecuteAfterCreateAsync<T>(T entity, CancellationToken ct = default) where T : class;
|
|
21
|
+
Task ExecuteBeforeUpdateAsync<T>(T entity, CancellationToken ct = default) where T : class;
|
|
22
|
+
Task ExecuteAfterUpdateAsync<T>(T entity, CancellationToken ct = default) where T : class;
|
|
23
|
+
Task ExecuteBeforeDeleteAsync<T>(T entity, CancellationToken ct = default) where T : class;
|
|
24
|
+
Task ExecuteAfterDeleteAsync<T>(T entity, CancellationToken ct = default) where T : class;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Semantics
|
|
29
|
+
|
|
30
|
+
- **`IBefore*`** runs **before** the entity is persisted. May transform the entity. May `throw` to cancel the operation (the calling handler does NOT call `SaveChangesAsync`).
|
|
31
|
+
- **`IAfter*`** runs **after** `SaveChangesAsync` completes. Side-effects only (notifications, indexing, caches, integrations). Throwing here does NOT roll back the DB write — wrap risky calls.
|
|
32
|
+
- **`Order`** (default 0) — lower runs earlier. Use `Order = -100` to pre-empt validation, `Order = 100` to run last.
|
|
33
|
+
|
|
34
|
+
## DI registration (Infrastructure DependencyInjection.cs)
|
|
35
|
+
|
|
36
|
+
```csharp
|
|
37
|
+
services.AddScoped<IHookExecutor, HookExecutor>();
|
|
38
|
+
services.AddScoped<IBeforeCreate<Employee>, EmployeeValidationHook>();
|
|
39
|
+
services.AddScoped<IAfterCreate<Employee>, EmployeeWelcomeNotificationHook>();
|
|
40
|
+
services.AddScoped<IAfterUpdate<Employee>, EmployeeUpdatedAuditHook>();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage in service / handler
|
|
44
|
+
|
|
45
|
+
```csharp
|
|
46
|
+
public async Task<EmployeeDto> CreateAsync(CreateEmployeeDto dto, CancellationToken ct)
|
|
47
|
+
{
|
|
48
|
+
var entity = Employee.Create(dto.TenantId, dto.Code, dto.Name);
|
|
49
|
+
await _hooks.ExecuteBeforeCreateAsync(entity, ct); // may throw → cancel
|
|
50
|
+
_db.Employees.Add(entity);
|
|
51
|
+
await _db.SaveChangesAsync(ct);
|
|
52
|
+
await _hooks.ExecuteAfterCreateAsync(entity, ct); // post-commit side effects
|
|
53
|
+
return _mapper.Map<EmployeeDto>(entity);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## DO / DON'T
|
|
58
|
+
|
|
59
|
+
- DO use hooks for cross-cutting concerns (notifications, audit beyond `IAuditableEntity`, cache invalidation)
|
|
60
|
+
- DO keep hooks small and idempotent — `IAfter*` runs without DB transaction
|
|
61
|
+
- DON'T use hooks for business validation that varies per command — keep that in the handler / FluentValidation
|
|
62
|
+
- DON'T register the same hook twice — DI picks all of them, and they all execute
|
|
63
|
+
|
|
64
|
+
## Sources
|
|
65
|
+
|
|
66
|
+
- `D:/01 - projets/SmartStack.app/features/IA-Workflow/src/SmartStack.Application/Common/Interfaces/Hooks/IBeforeCreate.cs`
|
|
67
|
+
- `D:/01 - projets/SmartStack.app/features/IA-Workflow/src/SmartStack.Application/Common/Interfaces/Hooks/IAfterCreate.cs`
|
|
68
|
+
- `D:/01 - projets/SmartStack.app/features/IA-Workflow/src/SmartStack.Application/Common/Interfaces/Hooks/IHookExecutor.cs` (and 4 more)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Licensing — `[RequiresLicense]` attribute (v3.46+)
|
|
2
|
+
|
|
3
|
+
> Extracted from `smartstack-api.md` for clarity. Loaded only when generating code that needs license gating.
|
|
4
|
+
|
|
5
|
+
Mark a Command or Query as requiring a valid license / feature / write capability.
|
|
6
|
+
|
|
7
|
+
## Attribute definition
|
|
8
|
+
|
|
9
|
+
```csharp
|
|
10
|
+
namespace SmartStack.Application.Common.Licensing;
|
|
11
|
+
|
|
12
|
+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
|
13
|
+
public class RequiresLicenseAttribute : Attribute
|
|
14
|
+
{
|
|
15
|
+
public string? Feature { get; }
|
|
16
|
+
public bool IsWriteOperation { get; }
|
|
17
|
+
|
|
18
|
+
public RequiresLicenseAttribute() { /* just needs valid license */ }
|
|
19
|
+
public RequiresLicenseAttribute(bool isWriteOperation) { /* blocked in read-only mode */ }
|
|
20
|
+
public RequiresLicenseAttribute(string feature, bool isWriteOperation = false) { /* both */ }
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage on a Command
|
|
25
|
+
|
|
26
|
+
```csharp
|
|
27
|
+
[RequiresLicense(LicenseFeatures.AdvancedWorkflows, isWriteOperation: true)]
|
|
28
|
+
public record CreateAdvancedWorkflowCommand(string Name, ...) : IRequest<WorkflowDto>;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Behavior at runtime
|
|
32
|
+
|
|
33
|
+
- A MediatR pipeline behavior reads the attribute on the request type.
|
|
34
|
+
- `Feature` → checked against the cached `License.Features` (resolved per-tenant from JWT).
|
|
35
|
+
- `IsWriteOperation = true` → blocked when the system is in read-only mode (license expired but in grace period, license downgraded, …).
|
|
36
|
+
- Failures throw a typed exception → 403 Forbidden with a stable error code (`LICENSE_FEATURE_DISABLED`, `LICENSE_READ_ONLY`).
|
|
37
|
+
|
|
38
|
+
## Where to read feature constants
|
|
39
|
+
|
|
40
|
+
`SmartStack.Domain.Licensing.LicenseFeatures` (string consts — `Entra`, `AdvancedWorkflows`, `AiAdvanced`, …).
|
|
41
|
+
|
|
42
|
+
## DO / DON'T
|
|
43
|
+
|
|
44
|
+
- DO put `[RequiresLicense]` on the request (Command/Query), not on the handler
|
|
45
|
+
- DO use `LicenseFeatures.X` constants — never magic strings
|
|
46
|
+
- DON'T add `[RequiresLicense]` on `[AllowAnonymous]` endpoints — they bypass the pipeline
|
|
47
|
+
- DON'T duplicate the check in the handler body — the pipeline already enforced it
|
|
48
|
+
|
|
49
|
+
## Sources
|
|
50
|
+
|
|
51
|
+
- `D:/01 - projets/SmartStack.app/features/IA-Workflow/src/SmartStack.Application/Common/Licensing/RequiresLicenseAttribute.cs`
|
|
52
|
+
- `D:/01 - projets/SmartStack.app/features/IA-Workflow/src/SmartStack.Domain/Licensing/LicenseFeatures.cs`
|
|
@@ -75,6 +75,7 @@ bash references/checks/infrastructure-checks.sh
|
|
|
75
75
|
| C37 | CRITICAL | Detail page tabs must NOT navigate() — content switches locally | frontend-checks.sh |
|
|
76
76
|
| C49 | BLOCKING | Route Ordering in App.tsx — static before dynamic | frontend-checks.sh |
|
|
77
77
|
| C52 | BLOCKING | Frontend route paths must include module segment | frontend-checks.sh |
|
|
78
|
+
| C64 | BLOCKING | ListPages must have Create/New button (navigate to create route) | frontend-checks.sh |
|
|
78
79
|
|
|
79
80
|
### Seed Data — Navigation, Roles, Permissions (C1-C2, C10, C15-C23, C32-C35, C44-C48, C53)
|
|
80
81
|
|
|
@@ -89,7 +90,7 @@ bash references/checks/infrastructure-checks.sh
|
|
|
89
90
|
| C18 | BLOCKING | Permissions.cs static constants must exist | seed-checks.sh |
|
|
90
91
|
| C19 | BLOCKING | ApplicationRolesSeedData.cs must exist | seed-checks.sh |
|
|
91
92
|
| C20 | WARNING | Section route completeness (NavigationSection → frontend route + permissions) | seed-checks.sh |
|
|
92
|
-
| C21 | WARNING | FORBIDDEN route patterns — /
|
|
93
|
+
| C21 | WARNING | FORBIDDEN route patterns — /detail/:id | seed-checks.sh |
|
|
93
94
|
| C22 | WARNING | Permission path segment count (2-4 dots expected) | seed-checks.sh |
|
|
94
95
|
| C23 | BLOCKING | IClientSeedDataProvider must have 4 methods with real implementation (not stubs) + DI registration | seed-checks.sh |
|
|
95
96
|
| C32 | CRITICAL | Translation seed data must have idempotency guard | seed-checks.sh |
|
|
@@ -104,6 +105,7 @@ bash references/checks/infrastructure-checks.sh
|
|
|
104
105
|
| C53 | BLOCKING | Enum serialization — JsonStringEnumConverter required | seed-checks.sh |
|
|
105
106
|
| C57 | BLOCKING | SeedDataProvider Seed*Async methods must NOT be `return Task.CompletedTask` or empty body — stubs cause empty menu | seed-checks.sh |
|
|
106
107
|
| C58 | BLOCKING | File paths in filesToCreate must be valid C# identifiers — no spaces, apostrophes, or accents | seed-checks.sh |
|
|
108
|
+
| C63 | BLOCKING | Reserved section codes (detail/create/edit/*-detail) must NOT be seeded as menu items | seed-checks.sh |
|
|
107
109
|
|
|
108
110
|
### Architecture — Clean Architecture Layer Isolation (A1-A8)
|
|
109
111
|
|
|
@@ -135,6 +137,21 @@ bash references/checks/infrastructure-checks.sh
|
|
|
135
137
|
| C60 | BLOCKING | Controllers must inject ISender _mediator, NOT direct business services | infrastructure-checks.sh |
|
|
136
138
|
| C61 | BLOCKING | Controllers with [NavRoute] must have [Authorize] attribute | infrastructure-checks.sh |
|
|
137
139
|
|
|
140
|
+
### PRD Compliance — Delegate Mode Only (PC-1 to PC-6)
|
|
141
|
+
|
|
142
|
+
> **These checks run ONLY in delegate mode** (when `/apex -d` is used with companion spec files).
|
|
143
|
+
> They validate that generated code matches the PRD specifications from `.ralph/` companion files.
|
|
144
|
+
> Implemented in `step-04-examine.md` section 6d — no separate .sh script needed.
|
|
145
|
+
|
|
146
|
+
| ID | Severity | Description | Source |
|
|
147
|
+
|----|----------|-------------|--------|
|
|
148
|
+
| PC-1 | BLOCKING | All columns from screens.json SmartTable resources must exist in generated ListPage | step-04 §6d |
|
|
149
|
+
| PC-2 | BLOCKING | All actions from screens.json (create, edit, delete, approve, reject, export) must have handlers in pages | step-04 §6d |
|
|
150
|
+
| PC-3 | WARNING | I18n labels must have correct UTF-8 accents (FR: Employés not Employes, DE: Übersicht not Ubersicht) | step-04 §6d |
|
|
151
|
+
| PC-4 | BLOCKING | All permissionPaths from permissions.json must exist in PermissionsSeedData.cs and Permissions.cs | step-04 §6d |
|
|
152
|
+
| PC-5 | WARNING | Every primary/functional section in screens.json must have a corresponding page file | step-04 §6d |
|
|
153
|
+
| PC-6 | WARNING | Module i18n namespace must be registered in i18n config (prevents raw key display) | step-04 §6d |
|
|
154
|
+
|
|
138
155
|
---
|
|
139
156
|
|
|
140
157
|
## Summary
|
|
@@ -16,10 +16,46 @@ public abstract class BaseEntity
|
|
|
16
16
|
public Guid Id { get; set; }
|
|
17
17
|
public DateTime CreatedAt { get; set; }
|
|
18
18
|
public DateTime? UpdatedAt { get; set; }
|
|
19
|
+
|
|
20
|
+
// v3.46+ : JSON storage for SDK extension fields. Default "{}".
|
|
21
|
+
public string ExtensionData { get; private set; } = "{}";
|
|
19
22
|
}
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
**
|
|
25
|
+
**4 inherited properties** (v3.46+). No Code, no IsDeleted, no RowVersion, no SoftDelete, no CreatedBy/UpdatedBy. `Code` is a business property — add it on the concrete entity. `CreatedBy`/`UpdatedBy` come from `IAuditableEntity` (separate interface).
|
|
26
|
+
|
|
27
|
+
### ExtensionData — SDK custom fields (v3.46+)
|
|
28
|
+
|
|
29
|
+
`BaseEntity.ExtensionData` is a JSON string allowing SDK clients to attach custom fields to ANY entity without schema migrations. The base class exposes 7 typed methods:
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
// Read a typed value
|
|
33
|
+
var color = entity.GetExtensionValue<string>("color");
|
|
34
|
+
var meta = entity.GetExtensionValue<MyMetadata>("meta");
|
|
35
|
+
|
|
36
|
+
// Write
|
|
37
|
+
entity.SetExtensionValue("color", "blue");
|
|
38
|
+
entity.SetExtensionValue("meta", new MyMetadata { Tag = "vip" });
|
|
39
|
+
|
|
40
|
+
// Inspect
|
|
41
|
+
if (entity.HasExtensionValue("color")) { ... }
|
|
42
|
+
var all = entity.GetAllExtensionData(); // IReadOnlyDictionary<string, JsonElement>
|
|
43
|
+
|
|
44
|
+
// Bulk replace
|
|
45
|
+
entity.SetAllExtensionData(new Dictionary<string, object?> { ["a"] = 1, ["b"] = 2 });
|
|
46
|
+
|
|
47
|
+
// Cleanup
|
|
48
|
+
entity.RemoveExtensionValue("color"); // returns bool
|
|
49
|
+
entity.ClearExtensionData(); // resets to "{}"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Conventions:**
|
|
53
|
+
- Serialization uses `JsonNamingPolicy.CamelCase` — `MyField` is stored as `"myField"`. Frontend consumers see camelCase.
|
|
54
|
+
- Mutating methods automatically update `UpdatedAt`.
|
|
55
|
+
- Backed by SQL Server `nvarchar(max)`. Map with `builder.Property(x => x.ExtensionData).HasColumnType("nvarchar(max)").HasDefaultValue("{}");` in EF config.
|
|
56
|
+
- `ExtensionData` is for **client SDK extensions** — NOT for storing application-controlled data (use real columns for those).
|
|
57
|
+
|
|
58
|
+
> Source : `D:/01 - projets/SmartStack.app/features/IA-Workflow/src/SmartStack.Domain/Common/BaseEntity.cs`
|
|
23
59
|
|
|
24
60
|
---
|
|
25
61
|
|
|
@@ -90,6 +126,38 @@ Verify in `Program.cs`: `JsonSerializerOptions.Converters.Add(new JsonStringEnum
|
|
|
90
126
|
|
|
91
127
|
---
|
|
92
128
|
|
|
129
|
+
## File Storage — `StorageType` enum (v3.46+)
|
|
130
|
+
|
|
131
|
+
```csharp
|
|
132
|
+
namespace SmartStack.Domain.Common;
|
|
133
|
+
|
|
134
|
+
public enum StorageType
|
|
135
|
+
{
|
|
136
|
+
Normal = 0, // Standard Azure Blob — deletable any time
|
|
137
|
+
Legal = 1 // Azure Legal Hold — immutable, 10-year retention (Swiss law Art. 958f CO)
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**When to use `Legal`:**
|
|
142
|
+
- Contracts, invoices, accounting documents (Swiss CO art. 958f → 10 years)
|
|
143
|
+
- Audit logs that must survive deletion attempts (compliance, GDPR Art. 30)
|
|
144
|
+
- Anything required by regulation to outlive the user's delete intent
|
|
145
|
+
|
|
146
|
+
**When `Normal` is enough:**
|
|
147
|
+
- User-uploaded avatars, attachments to tickets, message attachments, theme assets
|
|
148
|
+
- Anything the user owns and can legitimately delete
|
|
149
|
+
|
|
150
|
+
**Storage routing (Infrastructure):**
|
|
151
|
+
- `Normal` → standard container (deletable, free tier eligible)
|
|
152
|
+
- `Legal` → container with **Azure Blob immutable storage policy** + retention lock applied at upload
|
|
153
|
+
- The choice is made by the entity author when creating the file — not a runtime toggle
|
|
154
|
+
|
|
155
|
+
**AppSettings:** `appsettings.json` exposes both containers under `AzureStorage.Normal` and `AzureStorage.Legal` (with `EnableLegalHold` and `EnableRetentionPolicy` flags). Local dev uses `FileStorage.Normal/Legal` folders.
|
|
156
|
+
|
|
157
|
+
> Source : `D:/01 - projets/SmartStack.app/features/IA-Workflow/src/SmartStack.Domain/Common/StorageType.cs`
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
93
161
|
## Entity Patterns
|
|
94
162
|
|
|
95
163
|
### Tenant-scoped (most common)
|
|
@@ -333,6 +401,49 @@ public class {Name}Controller : ControllerBase
|
|
|
333
401
|
|
|
334
402
|
**Sub-resource completeness:** If a parent page has a navigate() to a sub-resource route, the frontend MUST include a page for that route. Otherwise → dead link → white screen. Prefer separate controllers (with Suffix) over sub-endpoints in parent controller.
|
|
335
403
|
|
|
404
|
+
### Core Controllers Exception (Bootstrap / Navigation / Config)
|
|
405
|
+
|
|
406
|
+
A small set of **bootstrap controllers** uses `[Route("api/[controller]")]` instead of `[NavRoute]`. These are routed BEFORE the navigation registry is built from the DB, so they cannot use NavRoute.
|
|
407
|
+
|
|
408
|
+
| Controller | Route | Reason |
|
|
409
|
+
|---|---|---|
|
|
410
|
+
| `BootstrapController` | `api/bootstrap` | Returns initial config (CSRF token, public flags) |
|
|
411
|
+
| `NavigationController` | `api/navigation` | Returns the navigation menu — must run before NavRoute resolves |
|
|
412
|
+
| `ConfigController` (if present) | `api/config` | Public app config (read-only) |
|
|
413
|
+
| `AuthController` | `api/auth` | Login/refresh — runs before authenticated routes resolve |
|
|
414
|
+
|
|
415
|
+
**Rules for core controllers:**
|
|
416
|
+
- Use the classical `[Route("api/[controller]")]` attribute
|
|
417
|
+
- Still apply `[Authorize]` and `[RequirePermission]` when applicable (most are anonymous-allowed)
|
|
418
|
+
- Do NOT add to navigation seed data
|
|
419
|
+
- POST-CHECK ignores these specific controller names when validating NavRoute usage
|
|
420
|
+
|
|
421
|
+
**Do not extend this list arbitrarily.** Application/business controllers MUST use `[NavRoute]`. If you think a new controller belongs to the bootstrap set, justify it: the controller must run before navigation is loaded.
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Entity Lifecycle Hooks (v3.46+)
|
|
426
|
+
|
|
427
|
+
6 typed hook interfaces + 1 executor in `SmartStack.Application.Common.Interfaces.Hooks` : `IBeforeCreate<T>`, `IAfterCreate<T>`, `IBeforeUpdate<T>`, `IAfterUpdate<T>`, `IBeforeDelete<T>`, `IAfterDelete<T>`, `IHookExecutor`. `IBefore*` may throw to cancel ; `IAfter*` runs post-commit (no rollback). Each has an `Order` property (default 0).
|
|
428
|
+
|
|
429
|
+
**See [entity-hooks-pattern.md](entity-hooks-pattern.md)** for full interface signatures, DI registration, usage example, and DO/DON'T list.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Domain Events (lightweight pub-sub)
|
|
434
|
+
|
|
435
|
+
Minimal pattern in `SmartStack.Domain.Support.Events` : `IDomainEvent { OccurredAt }` + `DomainEvent` abstract base. Use **events** for business facts independent of CRUD ; use **hooks** for cross-cutting reactions tied to the entity lifecycle. Dispatch is explicit (no auto-publication from `SaveChangesAsync` in v3.46).
|
|
436
|
+
|
|
437
|
+
**See [domain-events-pattern.md](domain-events-pattern.md)** for the full when-to-use table, naming convention, and dispatch example.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Licensing — `[RequiresLicense]` attribute (v3.46+)
|
|
442
|
+
|
|
443
|
+
Mark a Command or Query class as requiring a valid license, a specific feature (`LicenseFeatures.X`), or write capability (blocked in read-only mode). MediatR pipeline reads the attribute and throws 403 with stable error codes (`LICENSE_FEATURE_DISABLED`, `LICENSE_READ_ONLY`).
|
|
444
|
+
|
|
445
|
+
**See [licensing-enforcement.md](licensing-enforcement.md)** for the attribute definition (3 constructors), usage on Commands, runtime behavior, feature constants location, and DO/DON'T.
|
|
446
|
+
|
|
336
447
|
---
|
|
337
448
|
|
|
338
449
|
## Navigation Seed Data (routes must be full paths)
|
|
@@ -343,10 +454,10 @@ public class {Name}Controller : ControllerBase
|
|
|
343
454
|
| Module | `/{app-kebab}/{module-kebab}` | `/human-resources/employees` |
|
|
344
455
|
| Section | `/{app-kebab}/{module-kebab}/{section-kebab}` | `/human-resources/employees/departments` |
|
|
345
456
|
|
|
346
|
-
**Route
|
|
347
|
-
- `list` route = module route (e.g., `/human-resources/employees`)
|
|
348
|
-
- `detail` route
|
|
349
|
-
- Do NOT use: `/employees/
|
|
457
|
+
**Route convention:** ALL sections use uniform route = module route + `/{section-code}`:
|
|
458
|
+
- `list` route = module route + `/list` (e.g., `/human-resources/employees/list`)
|
|
459
|
+
- `detail` is an implicit route (not a section): module route + `/:id`
|
|
460
|
+
- Do NOT use: `/employees/detail/:id` (detail is implicit)
|
|
350
461
|
|
|
351
462
|
**Rules:**
|
|
352
463
|
- Routes ALWAYS start with `/`, include full hierarchy, use kebab-case
|
|
@@ -256,7 +256,7 @@ const applicationRoutes: ApplicationRouteExtensions = {
|
|
|
256
256
|
};
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
-
**Section-level routes:**
|
|
259
|
+
**Section-level routes:** ALL sections add their code as path segment (`list` → `/list`, `dashboard` → `/dashboard`, `approve` → `/approve`). `detail`/`create`/`edit` are implicit routes (`/:id`, `/create`, `/:id/edit`), NOT sections.
|
|
260
260
|
|
|
261
261
|
**PermissionGuard for sections:**
|
|
262
262
|
```tsx
|
|
@@ -175,12 +175,12 @@ var sectionRoute = $"{moduleRoute}/{ToKebabCase(sectionCode)}";
|
|
|
175
175
|
// → "/human-resources/employees/departments"
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
**Route
|
|
179
|
-
>
|
|
180
|
-
> - `list` section route = module route (e.g., `/human-resources/employees`)
|
|
181
|
-
> - `detail`
|
|
182
|
-
> -
|
|
183
|
-
> -
|
|
178
|
+
**Route convention:**
|
|
179
|
+
> ALL sections use a UNIFORM route pattern: module route + `/{section-code}`.
|
|
180
|
+
> - `list` section route = module route + `/list` (e.g., `/human-resources/employees/list`)
|
|
181
|
+
> - `detail` is an implicit route (not a section): module route + `/:id` (e.g., `/human-resources/employees/:id`)
|
|
182
|
+
> - Other sections (dashboard, approve, import) = module route + `/{section-kebab}` (same rule)
|
|
183
|
+
> - Do NOT use: `/employees/detail/:id` (detail is implicit, not a section code)
|
|
184
184
|
|
|
185
185
|
**Do not:**
|
|
186
186
|
- Use deterministic/sequential/fixed GUIDs — always use `Guid.NewGuid()`
|
|
@@ -164,7 +164,7 @@ IF precision_score < 2:
|
|
|
164
164
|
║ Your request looks more like a discovery/design task. ║
|
|
165
165
|
║ ║
|
|
166
166
|
║ Suggestions: ║
|
|
167
|
-
║ 1. Use /
|
|
167
|
+
║ 1. Use /business-analyse-quick to design your module ║
|
|
168
168
|
║ 2. Use /business-analyse for full analysis (40+ questions) ║
|
|
169
169
|
║ 3. Rewrite your prompt with entities and fields, e.g.: ║
|
|
170
170
|
║ /apex add employees section with Employee entity ║
|
|
@@ -100,6 +100,32 @@ IF existing_seed_files is NOT EMPTY:
|
|
|
100
100
|
|
|
101
101
|
---
|
|
102
102
|
|
|
103
|
+
### Reserved Section Code Guard (MANDATORY — runs before ANY section seeding)
|
|
104
|
+
|
|
105
|
+
> **CRITICAL RULE:** Reserved section codes must NOT be seeded as menu-visible sections.
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
RESERVED_SECTION_CODES = ["detail", "create", "edit"]
|
|
109
|
+
|
|
110
|
+
For each section in {sections} (from PRD or entity_section_map):
|
|
111
|
+
IF section.code IN RESERVED_SECTION_CODES OR section.code contains "-detail":
|
|
112
|
+
→ Do NOT seed this section in NavigationSeedData at all
|
|
113
|
+
→ "detail" is the /:id route of its parent list section (DynamicRouter convention)
|
|
114
|
+
→ "create" is the /create route (DynamicRouter convention)
|
|
115
|
+
→ "edit" is the /:id/edit route (DynamicRouter convention)
|
|
116
|
+
→ "*-detail" (e.g., "department-detail") is the /:id route of its parent section
|
|
117
|
+
→ Set permissionMode = "inherit" (no dedicated permissions)
|
|
118
|
+
→ LOG: "Skipped reserved section '{section.code}' — resolved by DynamicRouter convention"
|
|
119
|
+
|
|
120
|
+
ELSE:
|
|
121
|
+
→ Seed normally with IsActive = true
|
|
122
|
+
|
|
123
|
+
In delegate mode (-d): PRD data may contain reserved codes that were not challenged.
|
|
124
|
+
This guard catches them regardless of mode.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
103
129
|
### Per-Module Seed Data (CREATE or UPDATE)
|
|
104
130
|
|
|
105
131
|
#### If SEED_MODE = "CREATE"
|