@atlashub/smartstack-cli 4.76.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/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/references/checks/frontend-checks.sh +97 -11
- package/templates/skills/apex/references/checks/seed-checks.sh +34 -0
- package/templates/skills/apex/references/core-seed-data.md +7 -4
- 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/smartstack-api.md +112 -1
- package/templates/skills/apex-verify/steps/step-01-nav-audit.md +4 -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-seed.md +4 -2
- package/templates/skills/audit-route/references/routing-pattern.md +3 -1
- package/templates/skills/business-analyse/react/components.md +30 -28
- package/templates/skills/business-analyse/templates-react.md +15 -15
- 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/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/smoke-generation/SKILL.md +313 -0
- package/templates/skills/ui-components/SKILL.md +10 -0
- package/templates/skills/ui-components/references/component-catalog.md +82 -0
- package/templates/skills/workflow/SKILL.md +70 -1
package/package.json
CHANGED
|
@@ -96,7 +96,7 @@ Application → Module → Section → Resource
|
|
|
96
96
|
Permission: myapp.orders.{action}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
> **Note**: Applications have
|
|
99
|
+
> **Note (v3.46+)**: The legacy `ApplicationZone` enum has been removed. Applications now have two boolean flags : `IsPersonal` (default `false`) — when `true`, the app belongs to the user's personal scope (e.g. myspace) and bypasses tenant catalog filtering / permission checks ; `IsOpen` (default `false`) — when `true`, the app is accessible without permission checks (replaces the legacy hardcoded OPEN_APPS list in the frontend RouteGuard). Neither flag appears in routes or permission paths — they only steer access control.
|
|
100
100
|
|
|
101
101
|
### Permission Actions
|
|
102
102
|
`access` | `read` | `create` | `update` | `delete` | `export` | `import` | `approve` | `reject` | `assign` | `execute`
|
|
@@ -58,6 +58,61 @@ Configured instance: Code, Name, SystemContext, DefaultModel, MonthlyBudgetLimit
|
|
|
58
58
|
### OutputSchema
|
|
59
59
|
JSON Schema for validation: Code, Name, JsonSchema, DotNetType
|
|
60
60
|
|
|
61
|
+
### AiAgent (R17 — v3.46+)
|
|
62
|
+
Multi-step agent with reasoning modes. See [references/ai-agent-modes.md](references/ai-agent-modes.md).
|
|
63
|
+
|
|
64
|
+
- **AiAgentMode** : `Sequential` (0), `ReAct` (1), `PlanAndExecute` (2), `SelfCorrection` (3)
|
|
65
|
+
- **AiAgentStepRole** : `Worker` (0), `Planner` (1), `Reasoner` (2), `Critic` (3), `Synthesizer` (4)
|
|
66
|
+
- **MaxIterations** (default 5, range 1-20) — protects against infinite loops
|
|
67
|
+
- **QualityThreshold** (default 0.8) — used by `SelfCorrection` mode
|
|
68
|
+
- **AiSkill.CacheTtlSeconds** + **AiSkillBlock.IsCacheable** — caching for idempotent outputs
|
|
69
|
+
|
|
70
|
+
Default to `Sequential` ; switch to `ReAct`/`PlanAndExecute`/`SelfCorrection` only when needed (cost/latency impact).
|
|
71
|
+
|
|
72
|
+
### AiTool — function calling (R2 — v3.46+)
|
|
73
|
+
|
|
74
|
+
Server-side function the model can decide to invoke during a completion. Tools turn skills from one-shot prompts into agentic workflows that can read live data, call external APIs, and condition output on the result.
|
|
75
|
+
|
|
76
|
+
```csharp
|
|
77
|
+
class AiTool : BaseEntity, IAuditableEntity
|
|
78
|
+
{
|
|
79
|
+
public string Name { get; } // Snake_case, presented to model: "search_database"
|
|
80
|
+
public string DisplayName { get; } // Admin UI label
|
|
81
|
+
public string Description { get; } // Drives model decision — short, action-oriented
|
|
82
|
+
public string ParametersSchemaJson { get; } // JSON Schema of tool arguments
|
|
83
|
+
public string HandlerKey { get; } // Stable key → IToolHandler dispatcher
|
|
84
|
+
public Guid ModuleId { get; } // Module owns the tool (permissions + admin grouping)
|
|
85
|
+
public bool IsActive { get; }
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **Many-to-many** with `AiSkill` via `AiSkillTool` (kept as own entity for future per-attachment overrides : required flag, parameter aliasing, max-call budget).
|
|
90
|
+
- **HandlerKey** decouples definition from C# class name (handlers can be renamed without breaking the catalog).
|
|
91
|
+
- When `IsActive = false`, the orchestrator skips the tool even if a skill still references it.
|
|
92
|
+
|
|
93
|
+
**DO / DON'T:**
|
|
94
|
+
- DO author tool descriptions for the model — short, action-oriented, with preconditions
|
|
95
|
+
- DO snake_case the `Name` (LLM convention)
|
|
96
|
+
- DON'T expose long-running or heavy tools via function calling — prefer Workflow steps
|
|
97
|
+
- DON'T duplicate tool logic across skills — attach the same `AiTool` via `AiSkillTool`
|
|
98
|
+
|
|
99
|
+
### AiAgentExecution — runtime tracking
|
|
100
|
+
|
|
101
|
+
Each execution of an `AiAgent` is recorded via `AiAgentExecution` with:
|
|
102
|
+
- `Status` : `Running` / `Completed` / `Failed` / `PartiallyCompleted` / `Cancelled`
|
|
103
|
+
- `ExecutedSteps` / `TotalSteps`
|
|
104
|
+
- `InputStateJson` / `FinalStateJson` (audit trail)
|
|
105
|
+
- **Usage tracking** : `TotalInputTokens`, `TotalOutputTokens`, `TotalCost` (decimal), `TotalExecutionTimeMs`
|
|
106
|
+
- `_stepExecutions` : per-step `AiAgentStepExecution` (with own status, tokens, cost)
|
|
107
|
+
|
|
108
|
+
Service methods : `IncrementExecutedSteps()`, `MarkCompleted(json)`, `MarkFailed(msg)`, `MarkPartiallyCompleted(json, msg)`, `MarkCancelled(msg)`, `AccumulateUsage(in, out, cost, ms)`, `AddStepExecution(step)`.
|
|
109
|
+
|
|
110
|
+
**Use** : surface in admin UI (run history, cost dashboard, debug trace). Each agent run produces one `AiAgentExecution` + N `AiAgentStepExecution`.
|
|
111
|
+
|
|
112
|
+
### AI Evaluation Framework (R10)
|
|
113
|
+
|
|
114
|
+
For benchmarking skills against expected outputs, see [references/eval-framework.md](references/eval-framework.md). Pattern: `AiEvalDataset` (test cases) + `AiEvaluation` (one run) + `AiEvalResult` (per-item outcome). Provider/model agnostic — same dataset can compare 2 versions of the same skill code.
|
|
115
|
+
|
|
61
116
|
## Implementation
|
|
62
117
|
|
|
63
118
|
Start with [steps/step-00-init.md](steps/step-00-init.md) to gather requirements, then proceed to [steps/step-01-implementation.md](steps/step-01-implementation.md) for:
|
|
@@ -95,7 +150,16 @@ Start with [steps/step-00-init.md](steps/step-00-init.md) to gather requirements
|
|
|
95
150
|
| `Domain/AI/Prompts/Prompt.cs` | Prompt entity |
|
|
96
151
|
| `Domain/AI/Schemas/OutputSchema.cs` | Schema validation |
|
|
97
152
|
| `Domain/AI/AiProviderInstance.cs` | Configured instance |
|
|
153
|
+
| `Domain/AI/Agents/AiAgent.cs` | Multi-step agent (R17 — v3.46+) |
|
|
154
|
+
| `Domain/AI/Agents/AiAgentStep.cs` | Agent step with `Role` (Worker/Planner/Reasoner/Critic/Synthesizer) |
|
|
155
|
+
| `Domain/AI/Agents/AiAgentExecution.cs` | Run tracking (status, tokens, cost, step executions) |
|
|
156
|
+
| `Domain/AI/Skills/AiSkill.cs` | Reusable skill (`CacheTtlSeconds` for cache hint) |
|
|
157
|
+
| `Domain/AI/Tools/AiTool.cs` | Function-calling tool (R2 — v3.46+) |
|
|
158
|
+
| `Domain/AI/Tools/AiSkillTool.cs` | Many-to-many AiSkill ↔ AiTool |
|
|
159
|
+
| `Domain/AI/Evaluations/AiEvalDataset.cs` | Eval dataset (R10 — v3.46+) |
|
|
160
|
+
| `Domain/AI/Evaluations/AiEvaluation.cs` | Eval run with rollup stats |
|
|
98
161
|
| `Application/Common/Interfaces/IAiCompletionService.cs` | Service interface |
|
|
162
|
+
| `Application/Common/Interfaces/IToolHandler.cs` | Tool handler interface (resolved via HandlerKey) |
|
|
99
163
|
| `Infrastructure/Services/AI/AiCompletionService.cs` | Implementation |
|
|
100
164
|
| `web/src/services/api/aiApi.ts` | Frontend API |
|
|
101
165
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# AI Agent Modes & Step Roles (R17 — v3.46)
|
|
2
|
+
|
|
3
|
+
> **Reference for `ai-prompt` skill** — how to choose `AiAgentMode` and `AiAgentStepRole` when scaffolding multi-step AI agents.
|
|
4
|
+
|
|
5
|
+
## When This Reference Applies
|
|
6
|
+
|
|
7
|
+
- You create an `AiAgent` with more than one step
|
|
8
|
+
- You need a non-Sequential reasoning mode (loop, plan-then-execute, self-correct)
|
|
9
|
+
- The user asks "how do I make the agent retry", "how do I make it plan first", "what is ReAct"
|
|
10
|
+
|
|
11
|
+
For single-prompt skills, the basic `Prompt + Block + OutputSchema` pattern is enough — no agent, no mode.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## `AiAgentMode` (4 values)
|
|
16
|
+
|
|
17
|
+
| Mode | Value | Use case | Latency | Cost |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| `Sequential` | 0 | Known DAG fan-out / fan-in (analyze → summarize → email) | Low | Low |
|
|
20
|
+
| `ReAct` | 1 | Reasoning + tool loop, plan unknown ahead of time | High | High |
|
|
21
|
+
| `PlanAndExecute` | 2 | Task that can be planned upfront then executed | Medium | Medium |
|
|
22
|
+
| `SelfCorrection` | 3 | Output that can be critiqued and replayed | Medium | Medium-High |
|
|
23
|
+
|
|
24
|
+
### Recommended configuration
|
|
25
|
+
|
|
26
|
+
| Field | Default | Range |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `MaxIterations` | 5 | [1..20] — protects against infinite loops |
|
|
29
|
+
| `QualityThreshold` | 0.8 | [0..1] — used by `SelfCorrection` to decide replay |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## `AiAgentStepRole` (5 values)
|
|
34
|
+
|
|
35
|
+
| Role | Value | Expected output schema |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `Worker` | 0 | Standard skill output (used in `Sequential` mode) |
|
|
38
|
+
| `Planner` | 1 | `{ plan: [{ skillCode, label }] }` — produces the plan in `PlanAndExecute` |
|
|
39
|
+
| `Reasoner` | 2 | `{ action, input }` or `{ final_answer }` — used in `ReAct` |
|
|
40
|
+
| `Critic` | 3 | `{ score: 0..1, feedback?: string }` — used in `SelfCorrection` |
|
|
41
|
+
| `Synthesizer` | 4 | Final aggregated output (`PlanAndExecute` after parallel workers) |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Cache hint (R-cache)
|
|
46
|
+
|
|
47
|
+
| Field | Type | Purpose |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `AiSkill.CacheTtlSeconds` | `int?` | TTL for caching this skill's output (per input hash). Null = no cache. |
|
|
50
|
+
| `AiSkillBlock.IsCacheable` | `bool` | Marks an idempotent block whose output can be reused across executions. |
|
|
51
|
+
|
|
52
|
+
Only mark a skill / block cacheable when the output is **deterministic for a given input** — otherwise stale answers will be served.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Choosing a mode (decision tree)
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Question: do I know the steps in advance?
|
|
60
|
+
Yes → Sequential
|
|
61
|
+
No → Question: can the LLM decide the steps once, then execute?
|
|
62
|
+
Yes → PlanAndExecute
|
|
63
|
+
No → Question: must I let the LLM iterate with tools?
|
|
64
|
+
Yes → ReAct
|
|
65
|
+
No → SelfCorrection (single attempt + critique loop)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## DO / DON'T
|
|
71
|
+
|
|
72
|
+
| ✅ DO | ❌ DON'T |
|
|
73
|
+
|---|---|
|
|
74
|
+
| Start with `Sequential` — switch to other modes only when needed | Default to `ReAct` (expensive, hard to debug) |
|
|
75
|
+
| Cap `MaxIterations` at the smallest number that satisfies your case | Leave the default 5 for `ReAct` on long-running tasks (set 10-15) |
|
|
76
|
+
| Set `QualityThreshold` based on prompt evaluation results | Pick `0.8` blindly — measure on your eval dataset |
|
|
77
|
+
| Use `Critic` role with a structured output schema | Let the critic return free-form text |
|
|
78
|
+
| Cache only deterministic, idempotent skills | Cache anything that uses `now()` or randomness |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Reference source files (read-only)
|
|
83
|
+
|
|
84
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Agents\AiAgent.cs`
|
|
85
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Agents\AiAgentMode.cs`
|
|
86
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Agents\AiAgentStep.cs`
|
|
87
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Agents\AiAgentStepRole.cs`
|
|
88
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Skills\AiSkill.cs` (CacheTtlSeconds)
|
|
89
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Skills\AiSkillBlock.cs` (IsCacheable)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# AI Evaluation Framework (R10 — v3.46+)
|
|
2
|
+
|
|
3
|
+
> **Reference for `ai-prompt` skill** — how to evaluate AI skills against expected outputs.
|
|
4
|
+
|
|
5
|
+
## When This Reference Applies
|
|
6
|
+
|
|
7
|
+
- You are about to ship a new prompt or change an existing one
|
|
8
|
+
- You need to compare two versions of the same skill (`v1.0.0` vs `v1.1.0`)
|
|
9
|
+
- You want to benchmark the same prompt across providers/models (OpenAI vs Claude vs Gemini)
|
|
10
|
+
- The user asks "how do I test my prompt", "how do I prevent regression", "how do I A/B prompts"
|
|
11
|
+
|
|
12
|
+
For one-shot manual testing during development, the eval framework is overkill — call the skill directly. Use this framework when you want **reproducible, scored, auditable** runs.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## The 4 entities
|
|
17
|
+
|
|
18
|
+
### `AiEvalDataset` — collection of test cases
|
|
19
|
+
|
|
20
|
+
```csharp
|
|
21
|
+
class AiEvalDataset : BaseEntity, IAuditableEntity
|
|
22
|
+
{
|
|
23
|
+
public string Code { get; } // kebab-case stable id, e.g. "support-classification-v1"
|
|
24
|
+
public string Name { get; }
|
|
25
|
+
public string? Description { get; }
|
|
26
|
+
public IReadOnlyCollection<AiEvalDatasetItem> Items { get; }
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Provider/model agnostic** : the same dataset can be replayed against multiple skills, providers and models to compare quality and cost.
|
|
31
|
+
|
|
32
|
+
### `AiEvalDatasetItem` — one test case
|
|
33
|
+
|
|
34
|
+
```csharp
|
|
35
|
+
class AiEvalDatasetItem
|
|
36
|
+
{
|
|
37
|
+
public Guid DatasetId { get; }
|
|
38
|
+
public string Label { get; } // Human-readable name for the case
|
|
39
|
+
public string VariablesJson { get; } // Inputs to feed into the skill
|
|
40
|
+
public string ExpectedOutputJson { get; } // Expected result (for grading)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `AiEvaluation` — one run
|
|
45
|
+
|
|
46
|
+
```csharp
|
|
47
|
+
class AiEvaluation : BaseEntity, IAuditableEntity
|
|
48
|
+
{
|
|
49
|
+
public Guid SkillId { get; } // Which skill was tested
|
|
50
|
+
public Guid DatasetId { get; } // Which dataset was replayed
|
|
51
|
+
public AiEvaluationStatus Status { get; } // Running / Completed / Failed
|
|
52
|
+
public DateTime StartedAt { get; }
|
|
53
|
+
public DateTime? CompletedAt { get; }
|
|
54
|
+
public int ProcessedItems { get; }
|
|
55
|
+
public int TotalItems { get; }
|
|
56
|
+
public int PassedItems { get; } // Items whose actual output matched expected
|
|
57
|
+
public double? AverageScore { get; } // [0..1], null while in progress
|
|
58
|
+
public IReadOnlyCollection<AiEvalResult> Results { get; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
enum AiEvaluationStatus { Running = 0, Completed = 1, Failed = 2 }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Service methods : `RecordResult(AiEvalResult)`, `MarkCompleted()`, `MarkFailed(reason)`. The aggregate computes `AverageScore` from the per-item `Score` values when completed.
|
|
65
|
+
|
|
66
|
+
### `AiEvalResult` — per-item outcome
|
|
67
|
+
|
|
68
|
+
Records actual output vs expected, the score (0..1), pass/fail flag, and any tokens/cost incurred.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Workflow
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
1. Curate dataset
|
|
76
|
+
└─ AiEvalDataset.Create("support-classification-v1", "Support classification — 50 cases")
|
|
77
|
+
└─ AddItem("simple billing question", { "ticket": "..." }, { "category": "billing" })
|
|
78
|
+
└─ AddItem("urgent outage report", { "ticket": "..." }, { "category": "incident" })
|
|
79
|
+
└─ … 48 more items
|
|
80
|
+
|
|
81
|
+
2. Run evaluation against current skill version
|
|
82
|
+
└─ AiEvaluation.Create(skillId, datasetId, totalItems: 50)
|
|
83
|
+
└─ For each item: invoke skill, grade output, RecordResult(actual, expected, score)
|
|
84
|
+
└─ MarkCompleted()
|
|
85
|
+
└─ Stored AverageScore = 0.86 (43/50 passed)
|
|
86
|
+
|
|
87
|
+
3. Iterate — change prompt / blocks / model
|
|
88
|
+
└─ New AiEvaluation against same dataset
|
|
89
|
+
└─ Compare AverageScore : 0.86 → 0.92 (+0.06)
|
|
90
|
+
|
|
91
|
+
4. Decide
|
|
92
|
+
└─ If new score significantly higher : promote prompt version
|
|
93
|
+
└─ If equal or lower : revert
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## DO / DON'T
|
|
99
|
+
|
|
100
|
+
| ✅ DO | ❌ DON'T |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Build datasets from real production examples (anonymised) | Use synthetic / hallucinated test cases as the only ones |
|
|
103
|
+
| Stabilise dataset.Code — never rename, only add new datasets | Mutate dataset items after first run (compromises history) |
|
|
104
|
+
| Grade with structured comparison (JSON match, key fields, semantic distance) | Grade with substring search / regex |
|
|
105
|
+
| Track cost per evaluation — reject prompt changes that 3× cost for marginal score | Optimise for score alone |
|
|
106
|
+
| Run evals as part of the prompt-change PR review | Ship prompt changes without an eval baseline |
|
|
107
|
+
| Keep `AverageScore` as the single headline metric | Multiply scoring criteria — pick one and stick to it |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Wiring into a release pipeline
|
|
112
|
+
|
|
113
|
+
A prompt change should run the relevant evaluation as part of CI:
|
|
114
|
+
|
|
115
|
+
1. PR opens with a new prompt version (`v1.1.0`).
|
|
116
|
+
2. CI invokes a Command that creates an `AiEvaluation(skillId, datasetId, totalItems)` against `v1.1.0`.
|
|
117
|
+
3. CI compares `AverageScore` of `v1.1.0` vs the latest passing eval of `v1.0.0`.
|
|
118
|
+
4. PR is annotated with the delta. Score regression > 5% → PR blocked.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Reference source files (read-only)
|
|
123
|
+
|
|
124
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Evaluations\AiEvalDataset.cs`
|
|
125
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Evaluations\AiEvalDatasetItem.cs`
|
|
126
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Evaluations\AiEvaluation.cs`
|
|
127
|
+
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\AI\Evaluations\AiEvalResult.cs`
|
|
128
|
+
|
|
129
|
+
See also [ai-agent-modes.md](ai-agent-modes.md) for `MaxIterations` / `QualityThreshold` (used by `SelfCorrection` mode in conjunction with eval scores).
|
|
@@ -191,28 +191,74 @@ if [ -n "$ALL_PAGES" ]; then
|
|
|
191
191
|
fi
|
|
192
192
|
fi
|
|
193
193
|
|
|
194
|
-
# POST-CHECK C9: No hardcoded Tailwind colors in generated pages (
|
|
194
|
+
# POST-CHECK C9: No hardcoded Tailwind colors in generated pages (BLOCKING)
|
|
195
|
+
# v3.46+ : tightened enforcement. Theme system via CSS variables is mandatory.
|
|
195
196
|
ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
|
|
196
197
|
if [ -n "$ALL_PAGES" ]; then
|
|
197
|
-
|
|
198
|
+
# Extended palette: catches Tailwind 22 named colors + white/black
|
|
199
|
+
# Excludes status `text-white`/`text-black` ONLY when used as foreground over a CSS-var background (rare)
|
|
200
|
+
HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone|amber|yellow|orange|purple|indigo|violet|cyan|pink|emerald|rose|sky|teal|lime|fuchsia)-' $ALL_PAGES 2>/dev/null || true)
|
|
198
201
|
if [ -n "$HARDCODED" ]; then
|
|
199
|
-
echo "
|
|
200
|
-
echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes"
|
|
202
|
+
echo "BLOCKING (C9): Pages MUST use CSS variables instead of hardcoded Tailwind colors."
|
|
203
|
+
echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes."
|
|
201
204
|
echo ""
|
|
202
|
-
echo "Fix mapping:"
|
|
205
|
+
echo "Fix mapping (canonical SmartStack tokens):"
|
|
203
206
|
echo " bg-white → bg-[var(--bg-card)]"
|
|
204
207
|
echo " bg-gray-50 → bg-[var(--bg-primary)]"
|
|
208
|
+
echo " bg-gray-100 → bg-[var(--bg-secondary)]"
|
|
209
|
+
echo " bg-gray-200 → bg-[var(--bg-tertiary)]"
|
|
205
210
|
echo " text-gray-900 → text-[var(--text-primary)]"
|
|
206
|
-
echo " text-gray-
|
|
211
|
+
echo " text-gray-700 → text-[var(--text-secondary)]"
|
|
212
|
+
echo " text-gray-500 → text-[var(--text-tertiary)]"
|
|
213
|
+
echo " text-gray-400 → text-[var(--text-muted)]"
|
|
207
214
|
echo " border-gray-200 → border-[var(--border-color)]"
|
|
208
|
-
echo "
|
|
209
|
-
echo "
|
|
210
|
-
echo "
|
|
211
|
-
echo "
|
|
215
|
+
echo " border-gray-100 → border-[var(--border-subtle)]"
|
|
216
|
+
echo " bg-blue-* → bg-[var(--info-bg)] (semantic info)"
|
|
217
|
+
echo " bg-blue-600 → bg-[var(--color-accent-600)] (action button)"
|
|
218
|
+
echo " text-blue-600 → text-[var(--color-accent-600)]"
|
|
219
|
+
echo " text-red-* → text-[var(--error-text)]"
|
|
220
|
+
echo " bg-red-500/10 → bg-[var(--error-bg)]"
|
|
221
|
+
echo " text-yellow-* → text-[var(--warning-text)]"
|
|
222
|
+
echo " bg-amber-500/10 → bg-[var(--warning-bg)]"
|
|
223
|
+
echo " bg-green-* → bg-[var(--success-bg)]"
|
|
224
|
+
echo " text-green-* → text-[var(--success-text)]"
|
|
212
225
|
echo ""
|
|
213
|
-
echo "
|
|
226
|
+
echo "Status badges → use the 4 status token sets : --success-*, --warning-*, --error-*, --info-*"
|
|
227
|
+
echo " Example: 'bg-[var(--info-bg)] text-[var(--info-text)] border border-[var(--info-border)]'"
|
|
228
|
+
echo ""
|
|
229
|
+
echo "Avoid Tailwind \`dark:\` prefix — SmartStack toggles a \`.dark\` class on <html> and CSS vars adapt automatically."
|
|
230
|
+
echo ""
|
|
231
|
+
echo "References:"
|
|
232
|
+
echo " - templates/skills/application/references/themes-db-driven.md (full token list)"
|
|
233
|
+
echo " - templates/skills/ui-components/style-guide.md (DO/DON'T)"
|
|
214
234
|
echo ""
|
|
215
235
|
echo "$HARDCODED"
|
|
236
|
+
FAIL=true
|
|
237
|
+
fi
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# POST-CHECK C9b: Avoid Tailwind dark: prefix — SmartStack uses .dark class on root (BLOCKING)
|
|
241
|
+
ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
|
|
242
|
+
if [ -n "$ALL_PAGES" ]; then
|
|
243
|
+
DARK_PREFIX=$(grep -PnH '\bdark:(bg|text|border)-' $ALL_PAGES 2>/dev/null || true)
|
|
244
|
+
if [ -n "$DARK_PREFIX" ]; then
|
|
245
|
+
echo "BLOCKING (C9b): Tailwind 'dark:' prefix detected — incompatible with SmartStack theme system."
|
|
246
|
+
echo "SmartStack toggles a '.dark' class on <html> ; CSS vars (var(--bg-*), var(--text-*)) adapt automatically."
|
|
247
|
+
echo "Fix: remove 'dark:' prefix and rely on CSS vars instead."
|
|
248
|
+
echo "$DARK_PREFIX"
|
|
249
|
+
FAIL=true
|
|
250
|
+
fi
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
# POST-CHECK C9c: No hex colors in className (BLOCKING)
|
|
254
|
+
ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
|
|
255
|
+
if [ -n "$ALL_PAGES" ]; then
|
|
256
|
+
HEX_CN=$(grep -PnH 'className=[^>]*\[(#[0-9a-fA-F]{3,8})\]' $ALL_PAGES 2>/dev/null || true)
|
|
257
|
+
if [ -n "$HEX_CN" ]; then
|
|
258
|
+
echo "BLOCKING (C9c): Hex colors in className detected — must use CSS vars."
|
|
259
|
+
echo "Fix: replace #xxxxxx by var(--xxx) from the SmartStack token set."
|
|
260
|
+
echo "$HEX_CN"
|
|
261
|
+
FAIL=true
|
|
216
262
|
fi
|
|
217
263
|
fi
|
|
218
264
|
|
|
@@ -324,6 +370,46 @@ if [ -n "$TSX_PAGES" ] && [ -z "$DOC_DATA" ]; then
|
|
|
324
370
|
echo "The DocToggleButton in page headers will link to this documentation"
|
|
325
371
|
fi
|
|
326
372
|
|
|
373
|
+
# POST-CHECK C28: i18n namespace consistency across the 4 languages (BLOCKING)
|
|
374
|
+
# Each language directory must have the same set of JSON namespaces.
|
|
375
|
+
# Reference language is the one with the most files (usually fr or en).
|
|
376
|
+
I18N_BASE=$(find src/i18n/locales -maxdepth 1 -type d 2>/dev/null | grep -E "/(fr|en|it|de)$" || true)
|
|
377
|
+
if [ -n "$I18N_BASE" ]; then
|
|
378
|
+
# Build sorted file lists per language (basename only, .json suffix)
|
|
379
|
+
FR_FILES=$(find src/i18n/locales/fr -maxdepth 1 -name "*.json" -exec basename {} \; 2>/dev/null | sort -u || true)
|
|
380
|
+
EN_FILES=$(find src/i18n/locales/en -maxdepth 1 -name "*.json" -exec basename {} \; 2>/dev/null | sort -u || true)
|
|
381
|
+
IT_FILES=$(find src/i18n/locales/it -maxdepth 1 -name "*.json" -exec basename {} \; 2>/dev/null | sort -u || true)
|
|
382
|
+
DE_FILES=$(find src/i18n/locales/de -maxdepth 1 -name "*.json" -exec basename {} \; 2>/dev/null | sort -u || true)
|
|
383
|
+
|
|
384
|
+
# Pick the largest set as the canonical reference (excludes *_tmp.json which is intentionally orphan)
|
|
385
|
+
REF_FILES=$(echo -e "$FR_FILES\n$EN_FILES\n$IT_FILES\n$DE_FILES" | grep -v "_tmp\." | sort -u)
|
|
386
|
+
|
|
387
|
+
if [ -n "$REF_FILES" ]; then
|
|
388
|
+
FAIL_C28=false
|
|
389
|
+
for LANG in fr en it de; do
|
|
390
|
+
LANG_FILES=$(find src/i18n/locales/$LANG -maxdepth 1 -name "*.json" -exec basename {} \; 2>/dev/null | grep -v "_tmp\." | sort -u || true)
|
|
391
|
+
MISSING=$(comm -23 <(echo "$REF_FILES") <(echo "$LANG_FILES") || true)
|
|
392
|
+
if [ -n "$MISSING" ]; then
|
|
393
|
+
echo "BLOCKING (C28): i18n language '$LANG' is missing namespaces present in other languages:"
|
|
394
|
+
echo "$MISSING" | sed 's/^/ - /'
|
|
395
|
+
echo " Fix: create the missing JSON file(s) in src/i18n/locales/$LANG/ — even a stub {} is better than the runtime crash on a missing namespace."
|
|
396
|
+
FAIL_C28=true
|
|
397
|
+
fi
|
|
398
|
+
done
|
|
399
|
+
|
|
400
|
+
# Detect orphan _tmp.json files (intentional but should be cleaned up)
|
|
401
|
+
ORPHAN=$(find src/i18n/locales -name "*_tmp.json" 2>/dev/null || true)
|
|
402
|
+
if [ -n "$ORPHAN" ]; then
|
|
403
|
+
echo "WARNING (C28): orphan *_tmp.json file(s) detected — clean up after migration:"
|
|
404
|
+
echo "$ORPHAN" | sed 's/^/ - /'
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
if [ "$FAIL_C28" = true ]; then
|
|
408
|
+
FAIL=true
|
|
409
|
+
fi
|
|
410
|
+
fi
|
|
411
|
+
fi
|
|
412
|
+
|
|
327
413
|
# POST-CHECK C36: Frontend navigate() calls must have matching route definitions (CRITICAL)
|
|
328
414
|
PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null || true)
|
|
329
415
|
if [ -n "$PAGE_FILES" ]; then
|
|
@@ -571,6 +571,40 @@ if [ -n "$SEED_NAV_FILES" ]; then
|
|
|
571
571
|
done
|
|
572
572
|
fi
|
|
573
573
|
|
|
574
|
+
# POST-CHECK C66: ApplicationZone enum removed in v3.46 (BLOCKING)
|
|
575
|
+
# Detects any leftover reference to the removed ApplicationZone enum or Zone column.
|
|
576
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
577
|
+
DOMAIN_FILES=$(find src/ -path "*/Domain/Navigation/*" -name "*.cs" 2>/dev/null)
|
|
578
|
+
INFRA_FILES=$(find src/ -path "*/Infrastructure/Persistence/Configurations/Navigation/*" -name "*.cs" 2>/dev/null)
|
|
579
|
+
APP_FILES=$(find src/ -path "*/Application/Navigation/*" -name "*.cs" 2>/dev/null)
|
|
580
|
+
ALL_NAV_FILES="$SEED_FILES $DOMAIN_FILES $INFRA_FILES $APP_FILES"
|
|
581
|
+
if [ -n "$ALL_NAV_FILES" ]; then
|
|
582
|
+
BAD_ZONE=$(grep -Pn 'ApplicationZone\.\w+|public\s+ApplicationZone\s+Zone|HasColumnName\("Zone"\)|Zone\s*=\s*ApplicationZone' $ALL_NAV_FILES 2>/dev/null || true)
|
|
583
|
+
if [ -n "$BAD_ZONE" ]; then
|
|
584
|
+
echo "BLOCKING (C66): ApplicationZone enum has been removed in SmartStack v3.46."
|
|
585
|
+
echo "Replace by IsPersonal (bool, true => myspace scope) and/or IsOpen (bool, true => bypass permissions)."
|
|
586
|
+
echo "$BAD_ZONE"
|
|
587
|
+
echo "Fix: NavigationApplication.Create(code, label, ..., isOpen: false, isPersonal: false)"
|
|
588
|
+
echo "Reference: templates/skills/apex/references/core-seed-data.md (v3.46+ section)"
|
|
589
|
+
FAIL=true
|
|
590
|
+
fi
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
# POST-CHECK C67: Legacy domain-specific layouts removed in v3.46 (BLOCKING)
|
|
594
|
+
# AdminLayout / BusinessLayout / UserLayout / HRLayout / SalesLayout no longer exist.
|
|
595
|
+
TSX_FILES=$(find web/ -name "*.tsx" -not -path "*/node_modules/*" 2>/dev/null; find src/ -name "*.tsx" -not -path "*/node_modules/*" 2>/dev/null)
|
|
596
|
+
if [ -n "$TSX_FILES" ]; then
|
|
597
|
+
BAD_LAYOUT=$(grep -PnH '<\s*(AdminLayout|BusinessLayout|UserLayout|HRLayout|SalesLayout)\b|from\s+["'\''][^"'\'']*\/(AdminLayout|BusinessLayout|UserLayout|HRLayout|SalesLayout)["'\'']' $TSX_FILES 2>/dev/null || true)
|
|
598
|
+
if [ -n "$BAD_LAYOUT" ]; then
|
|
599
|
+
echo "BLOCKING (C67): Legacy layouts (AdminLayout/BusinessLayout/UserLayout/HRLayout/SalesLayout) have been removed in v3.46."
|
|
600
|
+
echo "Use AppLayout — the sole shell for authenticated application routes."
|
|
601
|
+
echo "$BAD_LAYOUT"
|
|
602
|
+
echo "Fix: Replace by <AppLayout /> (no domain-specific shell anymore)."
|
|
603
|
+
echo "Reference: templates/skills/application/references/frontend-route-naming.md"
|
|
604
|
+
FAIL=true
|
|
605
|
+
fi
|
|
606
|
+
fi
|
|
607
|
+
|
|
574
608
|
if [ "$FAIL" = true ]; then
|
|
575
609
|
exit 1
|
|
576
610
|
fi
|
|
@@ -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
|
|
|
@@ -1369,7 +1372,7 @@ Before marking the task as completed, verify ALL:
|
|
|
1369
1372
|
**Application-Level (FIRST — before modules):**
|
|
1370
1373
|
- [ ] `NavigationApplicationSeedData.cs` created (once per application, at `Infrastructure/Persistence/Seeding/Data/`)
|
|
1371
1374
|
- [ ] Application GUID is random (`Guid.NewGuid()`) — FK resolution is by Code lookup, not fixed ID
|
|
1372
|
-
- [ ] 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 = ...`
|
|
1373
1376
|
- [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application), using `app.Id` (actual DB ID) for EntityId
|
|
1374
1377
|
- [ ] `IClientSeedDataProvider.SeedNavigationAsync()` uses `NavigationApplicationSeedData` (NO hardcoded `{appLabel_en}` / `{appIcon}` placeholders)
|
|
1375
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)
|