@statedelta-actions/rules 0.5.1 → 0.5.2
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/CHANGELOG.md +14 -0
- package/README.md +316 -316
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
1
|
# @statedelta-actions/rules
|
|
2
2
|
|
|
3
|
-
> Rule Engine — superset
|
|
4
|
-
>
|
|
3
|
+
> Rule Engine — camada superset sobre o ActionEngine.
|
|
4
|
+
> Execução condicional de triggers por prioridade.
|
|
5
5
|
>
|
|
6
|
-
> Rule = trigger.
|
|
6
|
+
> Rule = trigger. Eventos são um package separado (`@statedelta-actions/events`).
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
1. [
|
|
13
|
-
2. [
|
|
14
|
-
3. [
|
|
15
|
-
4. [
|
|
16
|
-
5. [Rule
|
|
17
|
-
6. [
|
|
18
|
-
7. [
|
|
19
|
-
8. [Sub-rules (
|
|
20
|
-
9. [Hooks (
|
|
21
|
-
10. [Middleware (Params
|
|
22
|
-
11. [JIT
|
|
23
|
-
12. [Batch
|
|
24
|
-
13. [Async
|
|
10
|
+
## Índice
|
|
11
|
+
|
|
12
|
+
1. [Filosofia](#filosofia)
|
|
13
|
+
2. [Visão Geral da Arquitetura](#visão-geral-da-arquitetura)
|
|
14
|
+
3. [Instalação & Setup](#instalação--setup)
|
|
15
|
+
4. [Início Rápido](#início-rápido)
|
|
16
|
+
5. [Definição de Rule](#definição-de-rule)
|
|
17
|
+
6. [Registro](#registro)
|
|
18
|
+
7. [Avaliação de Triggers](#avaliação-de-triggers)
|
|
19
|
+
8. [Sub-rules (Cascata Condicional)](#sub-rules-cascata-condicional)
|
|
20
|
+
9. [Hooks (Governança)](#hooks-governança)
|
|
21
|
+
10. [Middleware (Enriquecimento de Params)](#middleware-enriquecimento-de-params)
|
|
22
|
+
11. [Compilação JIT](#compilação-jit)
|
|
23
|
+
12. [Operações em Batch](#operações-em-batch)
|
|
24
|
+
13. [Suporte Async](#suporte-async)
|
|
25
25
|
14. [Modo Interactive](#modo-interactive)
|
|
26
|
-
15. [
|
|
26
|
+
15. [Tratamento de Erros](#tratamento-de-erros)
|
|
27
27
|
16. [HALT_HANDLER](#halt_handler)
|
|
28
28
|
17. [createInvokerMiddleware](#createinvokermiddleware)
|
|
29
|
-
18. [
|
|
30
|
-
19. [
|
|
31
|
-
20. [
|
|
32
|
-
21. [
|
|
33
|
-
22. [Performance
|
|
34
|
-
23. [
|
|
29
|
+
18. [Referência Completa da API](#referência-completa-da-api)
|
|
30
|
+
19. [Caso de Uso: Tick de Combate RPG](#caso-de-uso-tick-de-combate-rpg)
|
|
31
|
+
20. [Caso de Uso: Pipeline de Pedido E-commerce](#caso-de-uso-pipeline-de-pedido-e-commerce)
|
|
32
|
+
21. [Caso de Uso: Workflow de Aprovação de Deploy](#caso-de-uso-workflow-de-aprovação-de-deploy)
|
|
33
|
+
22. [Espectro de Performance](#espectro-de-performance)
|
|
34
|
+
23. [Notas de Design Interno](#notas-de-design-interno)
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Filosofia
|
|
40
40
|
|
|
41
41
|
### Rule = Trigger
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Uma rule não é um executor. Uma rule é um **invocador** — um trigger condicional que, quando matched, invoca uma action através do ActionEngine. O RuleEngine não executa diretivas. Ele normaliza rules em hidden actions no momento do registro e depois delega toda a execução ao ActionEngine.
|
|
44
44
|
|
|
45
45
|
```
|
|
46
|
-
|
|
46
|
+
Registro:
|
|
47
47
|
rule { id: "combat-heal", when: ..., then: [...] }
|
|
48
48
|
-> actionEngine.register([{ id: "rule:combat-heal", directives: [...] }])
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Avaliação:
|
|
51
51
|
when(ctx) === true
|
|
52
52
|
-> actionEngine.invoke("rule:combat-heal", params)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Após o registro, o RuleEngine é um **loop de `when() -> invoke()`**.
|
|
56
56
|
|
|
57
|
-
### RuleEngine
|
|
57
|
+
### RuleEngine recebe, não cria
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
O RuleEngine recebe um `IActionEngine<TCtx>` já instanciado e configurado. Não injeta handlers, não manipula o access manifest, não cria o ActionEngine. O consumer é responsável por:
|
|
60
60
|
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
61
|
+
- Registrar handlers (`dispatch`, `emit`, `halt`, customizados)
|
|
62
|
+
- Definir limites, modo JIT
|
|
63
|
+
- Configurar o ActionAnalyzer separadamente se análise estática for necessária
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Essa separação significa que o mesmo ActionEngine pode servir tanto invocações diretas de action quanto invocações dirigidas por rules.
|
|
66
66
|
|
|
67
|
-
### Hooks =
|
|
67
|
+
### Hooks = governança, handlers = controle de fluxo
|
|
68
68
|
|
|
69
|
-
Hooks (`beforeRule`, `afterRule`)
|
|
69
|
+
Hooks (`beforeRule`, `afterRule`) governam o loop de rules: guards, observação, decisões de abort. Controle de fluxo **dentro** de uma action (halt, state locking, abort por erro) pertence aos handlers no ActionEngine. Duas camadas diferentes com responsabilidades diferentes.
|
|
70
70
|
|
|
71
|
-
### Middleware = params
|
|
71
|
+
### Middleware = enriquecimento de params, não transformação de ctx
|
|
72
72
|
|
|
73
|
-
Middleware
|
|
73
|
+
Middleware enriquece o envelope `params` (scope de execução), não o `ctx` de domínio. O `ctx` (TCtx) é estado de domínio owned pelo consumer — read-only para middleware.
|
|
74
74
|
|
|
75
|
-
###
|
|
75
|
+
### Nunca lança durante a avaliação
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
O engine nunca lança exceções para o consumer durante `evaluate()`. Todos os erros de runtime são coletados em `RuleEvaluationResult.errors`. A única exceção é um erro de programação: chamar `evaluate()` quando `isAsync === true`.
|
|
78
78
|
|
|
79
|
-
`register()`
|
|
79
|
+
`register()` é diferente — erros estruturais em boot-time (handler ausente para um tipo de diretiva) lançam um `Error` e abortam o registro atomicamente. Ver [Registro](#registro).
|
|
80
80
|
|
|
81
|
-
### Feature
|
|
81
|
+
### Feature não usada = zero overhead
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
Quando hooks não são registrados, o código de hook nunca é executado — não como branches mortos, mas como código ausente. O compilador JIT emite condicionalmente apenas o código das features que de fato estão configuradas. Zero middleware significa nenhuma variável de middleware, nenhuma chamada de pipeline, nenhuma alocação de params.
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
|
87
|
-
##
|
|
87
|
+
## Visão Geral da Arquitetura
|
|
88
88
|
|
|
89
|
-
### Monorepo
|
|
89
|
+
### Posição no Monorepo
|
|
90
90
|
|
|
91
91
|
```
|
|
92
|
-
@statedelta-actions/core <-
|
|
92
|
+
@statedelta-actions/core <- tipos compartilhados, slots, frame
|
|
93
93
|
|
|
|
94
|
-
@statedelta-actions/actions <- ActionEngine — runtime puro (
|
|
94
|
+
@statedelta-actions/actions <- ActionEngine — runtime puro (diretivas, handlers, JIT)
|
|
95
95
|
|
|
|
96
|
-
@statedelta-actions/rules <- RuleEngine (
|
|
97
|
-
@statedelta-actions/events <- EventProcessor (
|
|
96
|
+
@statedelta-actions/rules <- RuleEngine (este package)
|
|
97
|
+
@statedelta-actions/events <- EventProcessor (package separado)
|
|
98
98
|
|
|
99
|
-
@statedelta-actions/graph <-
|
|
100
|
-
@statedelta-actions/analyzer <- ActionAnalyzer —
|
|
99
|
+
@statedelta-actions/graph <- grafo de dependências (consumido pelo analyzer, não pelo actions/rules)
|
|
100
|
+
@statedelta-actions/analyzer <- ActionAnalyzer — análise estática, capabilities (opt-in)
|
|
101
101
|
|
|
102
|
-
(
|
|
102
|
+
(futuro) tick-runner / realm <- PPP: evaluate -> drain events -> repeat
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
###
|
|
105
|
+
### Modelo de Composição
|
|
106
106
|
|
|
107
107
|
```
|
|
108
|
-
Consumer (tick-runner, game loop,
|
|
108
|
+
Consumer (tick-runner, game loop, camada de negócio)
|
|
109
109
|
|
|
|
110
|
-
+--
|
|
111
|
-
+--
|
|
112
|
-
+--
|
|
113
|
-
+--
|
|
110
|
+
+-- configura o ActionEngine (handlers, limites)
|
|
111
|
+
+-- cria o RuleEngine(actionEngine, middleware, hooks)
|
|
112
|
+
+-- registra as rules
|
|
113
|
+
+-- chama evaluate(ctx)
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
###
|
|
116
|
+
### Estrutura de Módulos
|
|
117
117
|
|
|
118
118
|
```
|
|
119
119
|
src/
|
|
120
|
-
+-- types.ts <-
|
|
121
|
-
+-- engine.ts <- RuleEngineImpl + createRuleEngine + registry
|
|
120
|
+
+-- types.ts <- Todos os tipos e interfaces + RuleEvaluatorFn
|
|
121
|
+
+-- engine.ts <- RuleEngineImpl + createRuleEngine + helpers de registry (~470 linhas)
|
|
122
122
|
+-- validate.ts <- validateRule, validateSubRule
|
|
123
123
|
+-- handlers.ts <- HALT_HANDLER
|
|
124
|
-
+-- middleware.ts <- createInvokerMiddleware (
|
|
125
|
-
+-- index.ts <-
|
|
126
|
-
+-- eval/ <-
|
|
127
|
-
| +-- interpreter.ts <-
|
|
128
|
-
| +-- jit.ts <- buildRuleExecutor + emitReturn
|
|
124
|
+
+-- middleware.ts <- createInvokerMiddleware (público) + runMiddleware (interno)
|
|
125
|
+
+-- index.ts <- Re-exports públicos
|
|
126
|
+
+-- eval/ <- Runtime de avaliação
|
|
127
|
+
| +-- interpreter.ts <- Interpretador de avaliação de rules sync/async + builders de result
|
|
128
|
+
| +-- jit.ts <- buildRuleExecutor + codegen do emitReturn
|
|
129
129
|
| +-- sub-rules.ts <- evaluateSubRulesSync/Async
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
###
|
|
132
|
+
### Exports Públicos
|
|
133
133
|
|
|
134
134
|
```typescript
|
|
135
135
|
// Factory
|
|
@@ -141,7 +141,7 @@ export { HALT_HANDLER } from "./handlers";
|
|
|
141
141
|
// Middleware
|
|
142
142
|
export { createInvokerMiddleware } from "./middleware";
|
|
143
143
|
|
|
144
|
-
//
|
|
144
|
+
// Tipos
|
|
145
145
|
export type {
|
|
146
146
|
SubRuleDefinition,
|
|
147
147
|
RuleDefinition,
|
|
@@ -158,7 +158,7 @@ export type {
|
|
|
158
158
|
|
|
159
159
|
---
|
|
160
160
|
|
|
161
|
-
##
|
|
161
|
+
## Instalação & Setup
|
|
162
162
|
|
|
163
163
|
```typescript
|
|
164
164
|
import { createActionEngine } from "@statedelta-actions/actions";
|
|
@@ -168,7 +168,7 @@ import {
|
|
|
168
168
|
createInvokerMiddleware,
|
|
169
169
|
} from "@statedelta-actions/rules";
|
|
170
170
|
|
|
171
|
-
// 1. Configure ActionEngine
|
|
171
|
+
// 1. Configure o ActionEngine com seus handlers
|
|
172
172
|
const actionEngine = createActionEngine<MyCtx>({
|
|
173
173
|
handlers: {
|
|
174
174
|
dispatch: myDispatchHandler,
|
|
@@ -177,13 +177,13 @@ const actionEngine = createActionEngine<MyCtx>({
|
|
|
177
177
|
},
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
// 2.
|
|
180
|
+
// 2. Crie o RuleEngine, passando o ActionEngine
|
|
181
181
|
const ruleEngine = createRuleEngine<MyCtx>({
|
|
182
182
|
actionEngine,
|
|
183
|
-
middleware: [createInvokerMiddleware()], //
|
|
184
|
-
ruleHooks: { //
|
|
183
|
+
middleware: [createInvokerMiddleware()], // opcional
|
|
184
|
+
ruleHooks: { // opcional
|
|
185
185
|
beforeRule: (rule, evalCtx) => { /* guard */ },
|
|
186
|
-
afterRule: (rule, result, evalCtx) => { /*
|
|
186
|
+
afterRule: (rule, result, evalCtx) => { /* observa */ },
|
|
187
187
|
onRulesComplete: (result) => { /* cleanup */ },
|
|
188
188
|
},
|
|
189
189
|
maxSubRuleDepth: 10, // default
|
|
@@ -194,10 +194,10 @@ const ruleEngine = createRuleEngine<MyCtx>({
|
|
|
194
194
|
|
|
195
195
|
---
|
|
196
196
|
|
|
197
|
-
##
|
|
197
|
+
## Início Rápido
|
|
198
198
|
|
|
199
199
|
```typescript
|
|
200
|
-
//
|
|
200
|
+
// Registra as rules
|
|
201
201
|
ruleEngine.register([
|
|
202
202
|
{
|
|
203
203
|
id: "heal-when-low",
|
|
@@ -217,28 +217,28 @@ ruleEngine.register([
|
|
|
217
217
|
},
|
|
218
218
|
]);
|
|
219
219
|
|
|
220
|
-
//
|
|
220
|
+
// Avalia todas as rules de trigger contra o estado atual
|
|
221
221
|
const result = ruleEngine.evaluate(ctx);
|
|
222
222
|
|
|
223
|
-
// result.success -> true
|
|
224
|
-
// result.matched -> ["heal-when-low", "regen-mp"]
|
|
223
|
+
// result.success -> true se completou sem abort
|
|
224
|
+
// result.matched -> ["heal-when-low", "regen-mp"] (IDs das rules)
|
|
225
225
|
// result.counters -> { rulesEvaluated: 2, rulesMatched: 2, ... }
|
|
226
226
|
```
|
|
227
227
|
|
|
228
228
|
---
|
|
229
229
|
|
|
230
|
-
## Rule
|
|
230
|
+
## Definição de Rule
|
|
231
231
|
|
|
232
232
|
### SubRuleDefinition
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
Tipo base para sub-rules. Sem `priority` — sub-rules executam em **ordem de declaração**.
|
|
235
235
|
|
|
236
236
|
```typescript
|
|
237
237
|
interface SubRuleDefinition<TCtx> {
|
|
238
|
-
readonly id: string; //
|
|
239
|
-
readonly when?: (ctx: TCtx) => boolean; // trigger
|
|
240
|
-
readonly then?: readonly Directive<TCtx>[]; // action
|
|
241
|
-
readonly rules?: readonly SubRuleDefinition<TCtx>[]; //
|
|
238
|
+
readonly id: string; // identificador único
|
|
239
|
+
readonly when?: (ctx: TCtx) => boolean; // condição de trigger
|
|
240
|
+
readonly then?: readonly Directive<TCtx>[]; // diretivas da action
|
|
241
|
+
readonly rules?: readonly SubRuleDefinition<TCtx>[]; // sub-rules aninhadas
|
|
242
242
|
readonly tags?: readonly string[];
|
|
243
243
|
readonly effects?: readonly string[];
|
|
244
244
|
readonly declarations?: Record<string, unknown>;
|
|
@@ -248,65 +248,65 @@ interface SubRuleDefinition<TCtx> {
|
|
|
248
248
|
|
|
249
249
|
### RuleDefinition
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
Rules top-level estendem `SubRuleDefinition` com `priority` obrigatória.
|
|
252
252
|
|
|
253
253
|
```typescript
|
|
254
254
|
interface RuleDefinition<TCtx> extends SubRuleDefinition<TCtx> {
|
|
255
|
-
readonly priority: number; //
|
|
256
|
-
readonly rules?: readonly SubRuleDefinition<TCtx>[]; // sub-rules (
|
|
255
|
+
readonly priority: number; // maior = primeiro (ordem desc)
|
|
256
|
+
readonly rules?: readonly SubRuleDefinition<TCtx>[]; // sub-rules (sem priority)
|
|
257
257
|
}
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
Uma rule **precisa** ter `when` (condição de trigger).
|
|
261
261
|
|
|
262
|
-
|
|
262
|
+
Uma rule **precisa** ter `then` ou `rules` (ou ambos). Uma rule só com `rules` e sem `then` é um **group gate** — um container puramente condicional.
|
|
263
263
|
|
|
264
264
|
### Priority
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
Número maior = executa primeiro (como z-index). As rules são ordenadas por priority descendente no momento do registro. Sub-rules executam em **ordem de declaração** — elas não têm `priority`.
|
|
267
267
|
|
|
268
|
-
###
|
|
268
|
+
### Validação
|
|
269
269
|
|
|
270
|
-
| Check |
|
|
270
|
+
| Check | Código de Erro |
|
|
271
271
|
|-------|-----------|
|
|
272
|
-
| `id`
|
|
273
|
-
| `priority`
|
|
274
|
-
|
|
|
275
|
-
|
|
|
276
|
-
|
|
|
272
|
+
| `id` precisa ser uma string não vazia | `INVALID_RULE` |
|
|
273
|
+
| `priority` precisa ser um number | `INVALID_RULE` |
|
|
274
|
+
| Precisa ter função `when` | `INVALID_RULE` |
|
|
275
|
+
| Precisa ter `then` ou `rules` (ou ambos) | `INVALID_RULE` |
|
|
276
|
+
| ID duplicado | `DUPLICATE_ID` |
|
|
277
277
|
|
|
278
|
-
|
|
278
|
+
Validação de sub-rule:
|
|
279
279
|
|
|
280
|
-
| Check |
|
|
280
|
+
| Check | Código de Erro |
|
|
281
281
|
|-------|-----------|
|
|
282
|
-
| `id`
|
|
283
|
-
|
|
|
282
|
+
| `id` precisa ser uma string não vazia | `INVALID_RULE` |
|
|
283
|
+
| Precisa ter `then` ou `rules` (ou ambos) | `INVALID_RULE` |
|
|
284
284
|
|
|
285
285
|
---
|
|
286
286
|
|
|
287
|
-
##
|
|
287
|
+
## Registro
|
|
288
288
|
|
|
289
289
|
```typescript
|
|
290
290
|
const result = ruleEngine.register([rule1, rule2, ...]);
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
-
###
|
|
293
|
+
### Pipeline de registro
|
|
294
294
|
|
|
295
|
-
|
|
295
|
+
Três fases. Os registries locais só são mutados depois que o ActionEngine aceita todas as hidden actions — `register()` é atômico.
|
|
296
296
|
|
|
297
|
-
1. **Build** —
|
|
298
|
-
2. **
|
|
299
|
-
3. **
|
|
297
|
+
1. **Build** — valida cada rule (id, priority, when, then/rules), coleta sub-rules recursivamente, monta as definitions das hidden actions (`rule:{id}` para top-level, separadas por ponto para sub-rules). Erros de validação soft (id duplicado, campo obrigatório ausente) acumulam em `errors[]`. Ainda sem indexação local.
|
|
298
|
+
2. **Delegar ao ActionEngine** — `actionEngine.register(hiddenActions)`. Erros estruturais propagam como throw (handler ausente para um tipo de diretiva). Erros soft do ActionEngine são mapeados de volta para ids de rule e adicionados a `errors[]`.
|
|
299
|
+
3. **Indexar** — só é alcançado quando a fase 2 retorna. Insere cada rule em `_ruleRegistry` e `_rules` (ordenado por priority desc), refresca as flags `isAsync`/`isInteractive` per-rule a partir do mini-graph.
|
|
300
300
|
|
|
301
|
-
### Throw vs
|
|
301
|
+
### Throw vs erros coletados
|
|
302
302
|
|
|
303
|
-
|
|
|
303
|
+
| Erro | Comportamento |
|
|
304
304
|
|-------|----------|
|
|
305
|
-
|
|
|
306
|
-
|
|
|
307
|
-
| `validate()`
|
|
305
|
+
| Handler ausente para um tipo de diretiva em `then` (qualquer profundidade, incluindo sub-rules e `catch`) | **Lança `Error`** em `register()`. Nenhum estado local mutado. O consumer envolve em try/catch se precisar. |
|
|
306
|
+
| Id de rule duplicado, `when` ausente, `then`/`rules` ausentes, priority inválida | Coletado em `errors[]`. Outras rules válidas no mesmo call ainda registram. |
|
|
307
|
+
| `validate()` de um handler retorna invalid ou lança | Coletado em `errors[]`. |
|
|
308
308
|
|
|
309
|
-
|
|
309
|
+
O caminho de throw é reservado para erros estruturais de boot-time que não podem virar válidos em runtime — typos, handler esquecido no registro, resíduo de refactor. Erros soft são domain-level e merecem inspeção.
|
|
310
310
|
|
|
311
311
|
### RuleRegisterResult
|
|
312
312
|
|
|
@@ -321,36 +321,36 @@ interface RuleRegisterResult {
|
|
|
321
321
|
### Unregister
|
|
322
322
|
|
|
323
323
|
```typescript
|
|
324
|
-
const removed = ruleEngine.unregister("rule-id"); // true
|
|
324
|
+
const removed = ruleEngine.unregister("rule-id"); // true se encontrado e removido
|
|
325
325
|
```
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
Remove a rule de todos os registries internos, desregistra a hidden action do ActionEngine e limpa as sub-rules recursivamente.
|
|
328
328
|
|
|
329
329
|
---
|
|
330
330
|
|
|
331
|
-
##
|
|
331
|
+
## Avaliação de Triggers
|
|
332
332
|
|
|
333
333
|
```typescript
|
|
334
334
|
const result = ruleEngine.evaluate(ctx);
|
|
335
335
|
```
|
|
336
336
|
|
|
337
|
-
|
|
337
|
+
Processa todas as rules em ordem de priority descendente:
|
|
338
338
|
|
|
339
339
|
```
|
|
340
340
|
evaluate(ctx)
|
|
341
341
|
|
|
|
342
342
|
actionEngine.setContext(ctx)
|
|
343
343
|
|
|
|
344
|
-
|
|
344
|
+
para cada rule (priority desc):
|
|
345
345
|
|
|
|
346
|
-
+-- beforeRule
|
|
347
|
-
+-- when(ctx) -> false -> notMatched |
|
|
346
|
+
+-- hook beforeRule -> "skip" -> skipped | "abort" -> return | void -> continua
|
|
347
|
+
+-- when(ctx) -> false -> notMatched | erro -> coleta, notMatched
|
|
348
348
|
+-- matched
|
|
349
|
-
+--
|
|
350
|
-
+--
|
|
351
|
-
+-- afterRule
|
|
352
|
-
+--
|
|
353
|
-
+--
|
|
349
|
+
+-- Pipeline de middleware -> params (ou erro -> coleta, pula invoke)
|
|
350
|
+
+-- Invoca a action (se tem then) -> DirectiveResult
|
|
351
|
+
+-- hook afterRule -> "abort" -> return | void -> continua
|
|
352
|
+
+-- Check de halt -> aborted -> return
|
|
353
|
+
+-- Cascata de sub-rules -> aborted -> return
|
|
354
354
|
|
|
|
355
355
|
onRulesComplete(result)
|
|
356
356
|
return result
|
|
@@ -360,20 +360,20 @@ evaluate(ctx)
|
|
|
360
360
|
|
|
361
361
|
```typescript
|
|
362
362
|
interface RuleEvaluationResult {
|
|
363
|
-
readonly success: boolean; // true
|
|
364
|
-
readonly aborted: boolean; // true
|
|
363
|
+
readonly success: boolean; // true se completou sem abort
|
|
364
|
+
readonly aborted: boolean; // true se parou cedo
|
|
365
365
|
readonly abortedBy?: string; // "beforeRule" | "afterRule" | "sub-rule" | "halt" | handler
|
|
366
|
-
readonly matched: readonly string[]; // IDs
|
|
367
|
-
readonly skipped: readonly string[]; // IDs
|
|
366
|
+
readonly matched: readonly string[]; // IDs das rules matched
|
|
367
|
+
readonly skipped: readonly string[]; // IDs skipados pelo beforeRule
|
|
368
368
|
readonly notMatched: readonly string[];
|
|
369
369
|
readonly errors: readonly RuleError[];
|
|
370
|
-
readonly processedCount: number; // rules
|
|
370
|
+
readonly processedCount: number; // rules processadas (< totalCount em abort)
|
|
371
371
|
readonly totalCount: number;
|
|
372
|
-
readonly counters: FrameCounters; //
|
|
372
|
+
readonly counters: FrameCounters; // acumulado em todo o aninhamento
|
|
373
373
|
}
|
|
374
374
|
```
|
|
375
375
|
|
|
376
|
-
###
|
|
376
|
+
### Exemplo
|
|
377
377
|
|
|
378
378
|
```typescript
|
|
379
379
|
const ruleEngine = createRuleEngine({ actionEngine });
|
|
@@ -396,7 +396,7 @@ ruleEngine.register([
|
|
|
396
396
|
const ctx = { inCombat: true, hp: 30, defense: 0 };
|
|
397
397
|
const result = ruleEngine.evaluate(ctx);
|
|
398
398
|
|
|
399
|
-
// shield (200)
|
|
399
|
+
// shield (200) executa primeiro, depois heal (100)
|
|
400
400
|
// ctx.defense === 10, ctx.hp === 50
|
|
401
401
|
// result.matched === ["shield", "heal"]
|
|
402
402
|
// result.counters.rulesMatched === 2
|
|
@@ -404,42 +404,42 @@ const result = ruleEngine.evaluate(ctx);
|
|
|
404
404
|
|
|
405
405
|
---
|
|
406
406
|
|
|
407
|
-
## Sub-rules (
|
|
407
|
+
## Sub-rules (Cascata Condicional)
|
|
408
408
|
|
|
409
|
-
Sub-rules
|
|
409
|
+
Sub-rules permitem branching condicional dentro de uma rule. Depois que o `then` do pai executa, o `when()` de cada sub-rule é avaliado em **ordem de declaração**. Sub-rules podem aninhar em profundidade arbitrária (limitada por `maxSubRuleDepth`, default 10).
|
|
410
410
|
|
|
411
|
-
###
|
|
411
|
+
### Registro
|
|
412
412
|
|
|
413
|
-
Sub-rules
|
|
413
|
+
Sub-rules são registradas recursivamente como hidden actions com IDs separados por ponto:
|
|
414
414
|
|
|
415
415
|
```
|
|
416
|
-
rule:combat-heal <-
|
|
416
|
+
rule:combat-heal <- pai
|
|
417
417
|
rule:combat-heal.low-hp <- sub-rule
|
|
418
|
-
rule:combat-heal.low-hp.crit <-
|
|
418
|
+
rule:combat-heal.low-hp.crit <- sub-rule aninhada
|
|
419
419
|
```
|
|
420
420
|
|
|
421
|
-
Group gates (
|
|
421
|
+
Group gates (sem `then`) não registram action — `actionId = ""`.
|
|
422
422
|
|
|
423
|
-
###
|
|
423
|
+
### Avaliação
|
|
424
424
|
|
|
425
425
|
```
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
+-- when(ctx) -> false -> skip | undefined ->
|
|
429
|
-
+-- Invoke (
|
|
430
|
-
+--
|
|
426
|
+
Depois do invoke do pai (ou direto, para group gate):
|
|
427
|
+
para cada sub-rule (ordem de declaração):
|
|
428
|
+
+-- when(ctx) -> false -> skip | undefined -> incondicional
|
|
429
|
+
+-- Invoke (se tem then) -> aborted -> propaga pra cima
|
|
430
|
+
+-- Recursa (se tem sub-rules) -> aborted -> propaga pra cima
|
|
431
431
|
```
|
|
432
432
|
|
|
433
433
|
### Group gates
|
|
434
434
|
|
|
435
|
-
|
|
435
|
+
Um group gate é uma rule com `rules` mas sem `then`. Funciona como um container puramente condicional:
|
|
436
436
|
|
|
437
437
|
```typescript
|
|
438
438
|
{
|
|
439
439
|
id: "combat-group",
|
|
440
440
|
priority: 100,
|
|
441
441
|
when: (ctx) => ctx.zone === "combat",
|
|
442
|
-
//
|
|
442
|
+
// sem `then` — isto é um gate
|
|
443
443
|
rules: [
|
|
444
444
|
{ id: "heal", when: (ctx) => ctx.hp < 50, then: [...] },
|
|
445
445
|
{ id: "buff", when: (ctx) => ctx.mp > 0, then: [...] },
|
|
@@ -447,19 +447,19 @@ A group gate is a rule with `rules` but no `then`. It acts as a pure conditional
|
|
|
447
447
|
}
|
|
448
448
|
```
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
O gate avalia `when()`. Se true, os filhos são avaliados. Nenhuma action é invocada para o gate em si.
|
|
451
451
|
|
|
452
|
-
###
|
|
452
|
+
### Decisões de design
|
|
453
453
|
|
|
454
|
-
|
|
|
454
|
+
| Decisão | Racional |
|
|
455
455
|
|----------|-----------|
|
|
456
|
-
|
|
|
457
|
-
| Hooks
|
|
458
|
-
|
|
|
459
|
-
| Abort
|
|
460
|
-
|
|
|
456
|
+
| Ordem de declaração, não priority | Sub-rules são uma cascata dentro de uma rule — a ordem importa semanticamente |
|
|
457
|
+
| Hooks NÃO se aplicam a sub-rules | Hooks governam o loop principal, não o branching interno |
|
|
458
|
+
| Params de middleware herdados | Os `params` enriquecidos pelo middleware do pai propagam as-is |
|
|
459
|
+
| Abort propaga pra cima | Halt em sub-rule -> pai aborta -> loop principal para |
|
|
460
|
+
| Profundidade limitada | Default 10. Exceder -> erro coletado, sub-tree skipada (sem abort) |
|
|
461
461
|
|
|
462
|
-
###
|
|
462
|
+
### Exemplo: descontos por tier
|
|
463
463
|
|
|
464
464
|
```typescript
|
|
465
465
|
ruleEngine.register([
|
|
@@ -467,7 +467,7 @@ ruleEngine.register([
|
|
|
467
467
|
id: "discount-engine",
|
|
468
468
|
priority: 200,
|
|
469
469
|
when: (ctx) => ctx.status === "validated" && ctx.discount === 0,
|
|
470
|
-
then: [{ type: "log", message: "
|
|
470
|
+
then: [{ type: "log", message: "avaliando descontos" }],
|
|
471
471
|
rules: [
|
|
472
472
|
{
|
|
473
473
|
id: "vip-discount",
|
|
@@ -492,9 +492,9 @@ ruleEngine.register([
|
|
|
492
492
|
|
|
493
493
|
---
|
|
494
494
|
|
|
495
|
-
## Hooks (
|
|
495
|
+
## Hooks (Governança)
|
|
496
496
|
|
|
497
|
-
Hooks
|
|
497
|
+
Hooks são analisados uma vez no momento da construção via `analyzeSlots`. Eles governam apenas a avaliação de rules. Eventos têm seus próprios hooks em `@statedelta-actions/events`.
|
|
498
498
|
|
|
499
499
|
### Rule Hooks
|
|
500
500
|
|
|
@@ -506,11 +506,11 @@ ruleHooks: {
|
|
|
506
506
|
}
|
|
507
507
|
```
|
|
508
508
|
|
|
509
|
-
| Hook |
|
|
509
|
+
| Hook | Quando | Retornos | Comportamento de erro |
|
|
510
510
|
|------|------|---------|----------------|
|
|
511
|
-
| `beforeRule` |
|
|
512
|
-
| `afterRule` |
|
|
513
|
-
| `onRulesComplete` |
|
|
511
|
+
| `beforeRule` | Antes da avaliação do `when()` | `"skip"` -> skipped, `"abort"` -> para o pipeline, `void` -> continua | Coletado em `errors[]`, continua |
|
|
512
|
+
| `afterRule` | Depois do invoke (se a rule tinha `then`) | `"abort"` -> para o pipeline, `void` -> continua | Fire-and-forget |
|
|
513
|
+
| `onRulesComplete` | Depois de todas as rules processadas (ou abort) | `void` | Silenciado |
|
|
514
514
|
|
|
515
515
|
```typescript
|
|
516
516
|
interface RuleEvaluationContext<TCtx> {
|
|
@@ -519,7 +519,7 @@ interface RuleEvaluationContext<TCtx> {
|
|
|
519
519
|
}
|
|
520
520
|
```
|
|
521
521
|
|
|
522
|
-
###
|
|
522
|
+
### Exemplo: governança por priority
|
|
523
523
|
|
|
524
524
|
```typescript
|
|
525
525
|
const ruleEngine = createRuleEngine({
|
|
@@ -532,7 +532,7 @@ const ruleEngine = createRuleEngine({
|
|
|
532
532
|
});
|
|
533
533
|
```
|
|
534
534
|
|
|
535
|
-
###
|
|
535
|
+
### Exemplo: abort em erro
|
|
536
536
|
|
|
537
537
|
```typescript
|
|
538
538
|
ruleHooks: {
|
|
@@ -544,13 +544,13 @@ ruleHooks: {
|
|
|
544
544
|
|
|
545
545
|
---
|
|
546
546
|
|
|
547
|
-
## Middleware (Params
|
|
547
|
+
## Middleware (Enriquecimento de Params)
|
|
548
548
|
|
|
549
|
-
Middleware
|
|
549
|
+
Middleware roda entre o match e o invoke. Ele enriquece os `params` que viram o scope da action.
|
|
550
550
|
|
|
551
|
-
###
|
|
551
|
+
### Modelo de composição
|
|
552
552
|
|
|
553
|
-
|
|
553
|
+
Baseado em delta. Cada middleware recebe os params acumulados e retorna um delta para mesclar:
|
|
554
554
|
|
|
555
555
|
```
|
|
556
556
|
params0 = {} -> mw[0](rule, ctx, params0) -> d0 -> params1 = {...params0, ...d0}
|
|
@@ -559,9 +559,9 @@ params0 = {} -> mw[0](rule, ctx, params0) -> d0 -> params1 = {...params0, ...d0}
|
|
|
559
559
|
invoke(actionId, paramsN)
|
|
560
560
|
```
|
|
561
561
|
|
|
562
|
-
Middleware
|
|
562
|
+
Middleware não pode descartar o que os predecessores injetaram (só sobrescrever por chave).
|
|
563
563
|
|
|
564
|
-
###
|
|
564
|
+
### Assinatura
|
|
565
565
|
|
|
566
566
|
```typescript
|
|
567
567
|
type RuleMiddleware<TCtx> = (
|
|
@@ -571,15 +571,15 @@ type RuleMiddleware<TCtx> = (
|
|
|
571
571
|
) => Record<string, unknown>;
|
|
572
572
|
```
|
|
573
573
|
|
|
574
|
-
###
|
|
574
|
+
### Middleware de sub-rule
|
|
575
575
|
|
|
576
|
-
Middleware
|
|
576
|
+
Middleware **não** roda de novo para sub-rules. Os `params` enriquecidos pelo middleware do pai são passados diretamente para as invocações de sub-rule.
|
|
577
577
|
|
|
578
|
-
###
|
|
578
|
+
### Tratamento de erros
|
|
579
579
|
|
|
580
|
-
|
|
580
|
+
Erro de middleware -> coletado em `errors[]`, invoke skipado, a próxima rule continua.
|
|
581
581
|
|
|
582
|
-
###
|
|
582
|
+
### Exemplo: middleware de auditoria customizado
|
|
583
583
|
|
|
584
584
|
```typescript
|
|
585
585
|
const auditMiddleware: RuleMiddleware<MyCtx> = (rule, ctx, params) => ({
|
|
@@ -598,87 +598,87 @@ const ruleEngine = createRuleEngine({
|
|
|
598
598
|
|
|
599
599
|
---
|
|
600
600
|
|
|
601
|
-
## JIT
|
|
601
|
+
## Compilação JIT
|
|
602
602
|
|
|
603
|
-
JIT
|
|
603
|
+
O JIT compila o **loop de iteração de rules**, não diretivas individuais (o ActionEngine cuida do JIT de diretivas separadamente). Dois níveis de JIT coexistem: o ActionEngine compila a execução de diretivas, o RuleEngine compila a orquestração de rules.
|
|
604
604
|
|
|
605
|
-
###
|
|
605
|
+
### O que é compilado
|
|
606
606
|
|
|
607
|
-
`buildRuleExecutor`
|
|
607
|
+
`buildRuleExecutor` gera uma função via `new Function` que substitui o interpretador. Mesma assinatura — o engine troca transparentemente.
|
|
608
608
|
|
|
609
|
-
###
|
|
609
|
+
### Emissão condicional de código
|
|
610
610
|
|
|
611
|
-
|
|
611
|
+
O JS gerado contém apenas código para as features que de fato estão configuradas:
|
|
612
612
|
|
|
613
|
-
| Feature
|
|
613
|
+
| Feature ausente | Código não emitido |
|
|
614
614
|
|---|---|
|
|
615
|
-
| `beforeRule` |
|
|
616
|
-
| `afterRule` |
|
|
617
|
-
| `onRulesComplete` |
|
|
618
|
-
|
|
|
619
|
-
|
|
|
615
|
+
| `beforeRule` | Todo o bloco try/catch, checks de skip/abort |
|
|
616
|
+
| `afterRule` | Todo o bloco try/catch, check de abort |
|
|
617
|
+
| `onRulesComplete` | Chamada final + blocos de cleanup |
|
|
618
|
+
| Nenhum hook | `evalCtx` não criado, nenhuma variável de hook |
|
|
619
|
+
| Nenhum middleware | Variável `params` não declarada, nenhuma chamada de pipeline |
|
|
620
620
|
|
|
621
|
-
### Tier 0 —
|
|
621
|
+
### Tier 0 — loop minimal
|
|
622
622
|
|
|
623
|
-
|
|
623
|
+
Com zero hooks e zero middleware, o código gerado é um loop minimal:
|
|
624
624
|
|
|
625
625
|
```
|
|
626
|
-
for -> when(ctx) -> invoke(actionId) -> halt
|
|
626
|
+
for -> when(ctx) -> invoke(actionId) -> check de halt -> sub-rules -> próxima
|
|
627
627
|
```
|
|
628
628
|
|
|
629
|
-
|
|
629
|
+
Sem try/catch para hooks. Sem pipeline de middleware. Sem alocação de evalCtx.
|
|
630
630
|
|
|
631
|
-
###
|
|
631
|
+
### Modos
|
|
632
632
|
|
|
633
633
|
```typescript
|
|
634
634
|
const engine = createRuleEngine({ actionEngine, mode: "auto" });
|
|
635
635
|
```
|
|
636
636
|
|
|
637
|
-
|
|
|
637
|
+
| Modo | Comportamento |
|
|
638
638
|
|------|----------|
|
|
639
|
-
| `interpret` |
|
|
640
|
-
| `jit` |
|
|
641
|
-
| `auto` (default) |
|
|
639
|
+
| `interpret` | Sempre usa o interpretador. O JIT nunca ativa. `compile()` é no-op. |
|
|
640
|
+
| `jit` | Compila imediatamente na construção. Sem fase de interpretador. |
|
|
641
|
+
| `auto` (default) | Começa com o interpretador. Promove após N chamadas (threshold). |
|
|
642
642
|
|
|
643
|
-
|
|
643
|
+
Threshold default: 8 chamadas de `evaluate()`. Configurável via `autoJitThreshold`.
|
|
644
644
|
|
|
645
645
|
```
|
|
646
|
-
evaluate() #1..#7 ->
|
|
647
|
-
evaluate() #8 ->
|
|
648
|
-
evaluate() #9+ -> JIT
|
|
646
|
+
evaluate() #1..#7 -> interpretador
|
|
647
|
+
evaluate() #8 -> compila + troca interpretador por JIT
|
|
648
|
+
evaluate() #9+ -> JIT compilado
|
|
649
649
|
```
|
|
650
650
|
|
|
651
651
|
### compile()
|
|
652
652
|
|
|
653
653
|
```typescript
|
|
654
|
-
engine.compile(); //
|
|
654
|
+
engine.compile(); // força a promoção imediata para JIT
|
|
655
655
|
```
|
|
656
656
|
|
|
657
|
-
|
|
657
|
+
Útil para warmup. No-op se `mode === "interpret"`.
|
|
658
658
|
|
|
659
|
-
### compilationMode
|
|
659
|
+
### getter compilationMode
|
|
660
660
|
|
|
661
661
|
```typescript
|
|
662
662
|
engine.compilationMode // "interpret" | "jit"
|
|
663
663
|
```
|
|
664
664
|
|
|
665
|
-
|
|
665
|
+
Reflete o estado atual — muda de `"interpret"` para `"jit"` após a promoção.
|
|
666
666
|
|
|
667
667
|
---
|
|
668
668
|
|
|
669
|
-
## Batch
|
|
669
|
+
## Operações em Batch
|
|
670
670
|
|
|
671
|
-
###
|
|
671
|
+
### Baseado em callback (recomendado)
|
|
672
672
|
|
|
673
673
|
```typescript
|
|
674
674
|
const result = engine.batch((eng) => {
|
|
675
675
|
eng.register([rule1, rule2]);
|
|
676
676
|
eng.register([rule3, rule4]);
|
|
677
677
|
});
|
|
678
|
-
// endBatch()
|
|
678
|
+
// endBatch() chamado automaticamente, inclusive em throw
|
|
679
679
|
```
|
|
680
680
|
|
|
681
|
-
`batch(fn)`
|
|
681
|
+
`batch(fn)` garante o cleanup: se `fn` lança, `endBatch()` ainda é chamado para não deixar o engine num estado inconsistente.
|
|
682
682
|
|
|
683
683
|
### Manual
|
|
684
684
|
|
|
@@ -689,16 +689,16 @@ engine.register([rule3, rule4]);
|
|
|
689
689
|
const result = engine.endBatch();
|
|
690
690
|
```
|
|
691
691
|
|
|
692
|
-
-
|
|
693
|
-
-
|
|
694
|
-
-
|
|
695
|
-
-
|
|
692
|
+
- Delega `beginBatch()`/`endBatch()` ao ActionEngine
|
|
693
|
+
- Acumula os results de registro através de múltiplas chamadas de `register()`
|
|
694
|
+
- Mescla tudo no `endBatch()`
|
|
695
|
+
- Suporta aninhamento: `endBatch()` interno retorna vazio, o externo mescla tudo
|
|
696
696
|
|
|
697
697
|
---
|
|
698
698
|
|
|
699
|
-
## Async
|
|
699
|
+
## Suporte Async
|
|
700
700
|
|
|
701
|
-
###
|
|
701
|
+
### Detecção (per-rule transitivo)
|
|
702
702
|
|
|
703
703
|
```typescript
|
|
704
704
|
const isAsync = ruleHookAnalysis.hasAnyAsync || hasAnyAsyncRuleTransitive;
|
|
@@ -709,7 +709,7 @@ Refrescado em `register()` / `unregister()` / `endBatch()` consultando o **mini-
|
|
|
709
709
|
**Diferença importante:** antes era `actionEngine.isAsync` (global). Agora é per-rule transitivo. Engine híbrido (handler async + handler sync) com rules **100% sync na sub-árvore** permite `evaluate()` regular — só rules transitivamente async forçam `evaluateAsync()`.
|
|
710
710
|
|
|
711
711
|
```typescript
|
|
712
|
-
// Engine híbrido — handler async existe, mas action `log` (usada em syncRule) é sync
|
|
712
|
+
// Engine híbrido — handler async existe, mas a action `log` (usada em syncRule) é sync
|
|
713
713
|
const engine = createActionEngine({
|
|
714
714
|
handlers: { log, fetchAsync: { async: true, async execute() {...} } },
|
|
715
715
|
});
|
|
@@ -720,23 +720,23 @@ rules.register([
|
|
|
720
720
|
]);
|
|
721
721
|
|
|
722
722
|
engine.isAsync; // true (engine global async — fetchAsync existe)
|
|
723
|
-
rules.isAsync; // false (rule específica é sync transitivamente)
|
|
723
|
+
rules.isAsync; // false (a rule específica é sync transitivamente)
|
|
724
724
|
rules.evaluate(ctx); // ✅ funciona — não força evaluateAsync
|
|
725
725
|
```
|
|
726
726
|
|
|
727
|
-
###
|
|
727
|
+
### Uso
|
|
728
728
|
|
|
729
729
|
```typescript
|
|
730
|
-
// Sync (
|
|
730
|
+
// Sync (lança se isAsync)
|
|
731
731
|
const result = engine.evaluate(ctx);
|
|
732
732
|
|
|
733
|
-
// Async (
|
|
733
|
+
// Async (sempre funciona)
|
|
734
734
|
const result = await engine.evaluateAsync(ctx);
|
|
735
735
|
```
|
|
736
736
|
|
|
737
|
-
|
|
737
|
+
Chamar `evaluate()` quando `isAsync === true` lança um erro — é preciso usar `evaluateAsync()`. Isso é um guard contra descartar promises por acidente.
|
|
738
738
|
|
|
739
|
-
###
|
|
739
|
+
### Hooks async
|
|
740
740
|
|
|
741
741
|
```typescript
|
|
742
742
|
const ruleEngine = createRuleEngine({
|
|
@@ -753,7 +753,7 @@ const ruleEngine = createRuleEngine({
|
|
|
753
753
|
});
|
|
754
754
|
|
|
755
755
|
|
|
756
|
-
//
|
|
756
|
+
// Precisa usar evaluateAsync
|
|
757
757
|
const result = await ruleEngine.evaluateAsync(ctx);
|
|
758
758
|
```
|
|
759
759
|
|
|
@@ -761,17 +761,17 @@ const result = await ruleEngine.evaluateAsync(ctx);
|
|
|
761
761
|
|
|
762
762
|
## Modo Interactive
|
|
763
763
|
|
|
764
|
-
Permite **pausar avaliação de rules** e aguardar input externo via generators — espelhamento do modo interactive do ActionEngine. Ortogonal a sync/async.
|
|
764
|
+
Permite **pausar a avaliação de rules** e aguardar input externo via generators — espelhamento do modo interactive do ActionEngine. Ortogonal a sync/async.
|
|
765
765
|
|
|
766
|
-
**Habilitação:** ActionEngine precisa estar em modo interactive (`createActionEngine({ ..., interactive: {} })`). Rules detecta automaticamente quando alguma rule registrada tem ação transitivamente interactive.
|
|
766
|
+
**Habilitação:** o ActionEngine precisa estar em modo interactive (`createActionEngine({ ..., interactive: {} })`). O Rules detecta automaticamente quando alguma rule registrada tem ação transitivamente interactive.
|
|
767
767
|
|
|
768
|
-
###
|
|
768
|
+
### Detecção per-rule
|
|
769
769
|
|
|
770
770
|
`StoredRule.isInteractive` cacheia o resultado de `engine.isActionInteractive(rule.actionId)`. Refrescado em register/unregister/endBatch.
|
|
771
771
|
|
|
772
772
|
```typescript
|
|
773
773
|
rules.isInteractive; // true se qualquer rule é interactive transitivamente
|
|
774
|
-
rules.evaluate(ctx); //
|
|
774
|
+
rules.evaluate(ctx); // lança "use evaluateInteractive"
|
|
775
775
|
rules.evaluateAsync(ctx); // idem
|
|
776
776
|
rules.evaluateInteractive(ctx); // ✅ retorna iterator
|
|
777
777
|
```
|
|
@@ -816,9 +816,9 @@ session.next("Anderson"); // → { done: true, value: RuleEvaluationResult }
|
|
|
816
816
|
// ctx.out: ["before", "Anderson (yes)"]
|
|
817
817
|
```
|
|
818
818
|
|
|
819
|
-
### Mistura sync + interactive (priority
|
|
819
|
+
### Mistura sync + interactive (ordem de priority)
|
|
820
820
|
|
|
821
|
-
Rules sync e interactive coexistem na mesma
|
|
821
|
+
Rules sync e interactive coexistem na mesma avaliação, respeitando a priority. Rules sync rodam direto (sem yield); rules interactive pausam.
|
|
822
822
|
|
|
823
823
|
```typescript
|
|
824
824
|
rules.register([
|
|
@@ -857,7 +857,7 @@ await session.next(); // loadData rodou (fetch async), getName yieldou
|
|
|
857
857
|
await session.next("Anderson"); // matched: ["loadData", "getName"]
|
|
858
858
|
```
|
|
859
859
|
|
|
860
|
-
### Compilação JIT generator
|
|
860
|
+
### Compilação do JIT generator
|
|
861
861
|
|
|
862
862
|
Em `mode: "jit"`, `evaluateInteractive` usa **JIT generator compilado** (Fase R6 — `buildInteractiveRuleExecutor`). 4 wrappers possíveis:
|
|
863
863
|
|
|
@@ -868,7 +868,7 @@ Em `mode: "jit"`, `evaluateInteractive` usa **JIT generator compilado** (Fase R6
|
|
|
868
868
|
| false | true | `function* ()` (`evaluateInteractive` sync) |
|
|
869
869
|
| true | true | `async function* ()` (`evaluateInteractive` async) |
|
|
870
870
|
|
|
871
|
-
|
|
871
|
+
Branch emitido per-rule no loop do JIT:
|
|
872
872
|
|
|
873
873
|
```javascript
|
|
874
874
|
if (stored.isInteractive) {
|
|
@@ -880,29 +880,29 @@ if (stored.isInteractive) {
|
|
|
880
880
|
}
|
|
881
881
|
```
|
|
882
882
|
|
|
883
|
-
Decisão **per-rule em runtime**, baseada nas flags cached em `StoredRule.isAsync`/`isInteractive`. Em `mode: "interpret"` ou `"auto"` (antes do threshold), usa interpreter generator (mesmo
|
|
883
|
+
Decisão **per-rule em runtime**, baseada nas flags cached em `StoredRule.isAsync`/`isInteractive`. Em `mode: "interpret"` ou `"auto"` (antes do threshold), usa o interpreter generator (mesmo comportamento, sem a performance do unrolled).
|
|
884
884
|
|
|
885
885
|
---
|
|
886
886
|
|
|
887
|
-
##
|
|
887
|
+
## Tratamento de Erros
|
|
888
888
|
|
|
889
|
-
|
|
889
|
+
O engine **nunca lança** durante a avaliação. Todos os erros de runtime são coletados:
|
|
890
890
|
|
|
891
|
-
|
|
|
891
|
+
| Origem do erro | Comportamento |
|
|
892
892
|
|-------------|----------|
|
|
893
|
-
| `when()`
|
|
894
|
-
| `beforeRule`
|
|
895
|
-
| `afterRule`
|
|
896
|
-
| `onRulesComplete`
|
|
897
|
-
| Middleware
|
|
898
|
-
|
|
|
899
|
-
|
|
|
900
|
-
|
|
|
901
|
-
| `maxSubRuleDepth`
|
|
893
|
+
| `when()` lança | Erro coletado, rule tratada como `notMatched`, continua |
|
|
894
|
+
| `beforeRule` lança | Erro coletado, continua (tratado como void) |
|
|
895
|
+
| `afterRule` lança | Fire-and-forget, continua |
|
|
896
|
+
| `onRulesComplete` lança | Silenciado |
|
|
897
|
+
| Middleware lança | Erro coletado, invoke skipado, próxima rule |
|
|
898
|
+
| Erros de invoke do ActionEngine | Erros de diretiva acumulados nos counters |
|
|
899
|
+
| Id de hidden action ausente no momento do invoke | Pipeline aborta com `abortedBy: "action-not-found"`, `success: false` |
|
|
900
|
+
| `when()` de sub-rule lança | Erro coletado, sub-rule skipada |
|
|
901
|
+
| `maxSubRuleDepth` excedido | Erro coletado, sub-tree skipada (sem abort) |
|
|
902
902
|
|
|
903
|
-
`register()`
|
|
903
|
+
`register()` segue regras diferentes — ver [Registro](#registro). Erros estruturais em boot (handler ausente) lançam fail-fast.
|
|
904
904
|
|
|
905
|
-
###
|
|
905
|
+
### Inspecionando erros
|
|
906
906
|
|
|
907
907
|
```typescript
|
|
908
908
|
const result = engine.evaluate(ctx);
|
|
@@ -911,9 +911,9 @@ for (const err of result.errors) {
|
|
|
911
911
|
console.log(`Rule index ${err.ruleIndex}: ${err.error}`);
|
|
912
912
|
}
|
|
913
913
|
|
|
914
|
-
//
|
|
914
|
+
// O consumer decide o que os erros significam:
|
|
915
915
|
if (result.errors.length > 0) {
|
|
916
|
-
//
|
|
916
|
+
// trata os erros
|
|
917
917
|
}
|
|
918
918
|
```
|
|
919
919
|
|
|
@@ -921,7 +921,7 @@ if (result.errors.length > 0) {
|
|
|
921
921
|
|
|
922
922
|
## HALT_HANDLER
|
|
923
923
|
|
|
924
|
-
|
|
924
|
+
Um handler built-in que sinaliza ao ActionEngine para abortar a execução de diretivas. O interpretador do RuleEngine checa `abortedBy === "halt"` como uma parada controlada.
|
|
925
925
|
|
|
926
926
|
```typescript
|
|
927
927
|
import { HALT_HANDLER } from "@statedelta-actions/rules";
|
|
@@ -934,7 +934,7 @@ const actionEngine = createActionEngine({
|
|
|
934
934
|
});
|
|
935
935
|
```
|
|
936
936
|
|
|
937
|
-
|
|
937
|
+
Uso em diretivas:
|
|
938
938
|
|
|
939
939
|
```typescript
|
|
940
940
|
{
|
|
@@ -943,22 +943,22 @@ Usage in directives:
|
|
|
943
943
|
when: (ctx) => ctx.hp <= 0,
|
|
944
944
|
then: [
|
|
945
945
|
{ type: "dispatch", target: "status", op: "set", value: "dead" },
|
|
946
|
-
{ type: "halt" }, //
|
|
946
|
+
{ type: "halt" }, // para todas as rules restantes
|
|
947
947
|
],
|
|
948
948
|
}
|
|
949
949
|
```
|
|
950
950
|
|
|
951
|
-
|
|
951
|
+
Quando o halt dispara:
|
|
952
952
|
- `result.aborted === true`
|
|
953
953
|
- `result.abortedBy === "halt"`
|
|
954
|
-
- `result.success === true` (halt
|
|
955
|
-
-
|
|
954
|
+
- `result.success === true` (halt é uma parada controlada, não um erro)
|
|
955
|
+
- As rules restantes não são avaliadas
|
|
956
956
|
|
|
957
957
|
---
|
|
958
958
|
|
|
959
959
|
## createInvokerMiddleware
|
|
960
960
|
|
|
961
|
-
|
|
961
|
+
Middleware built-in, opt-in, que injeta metadados `$invoker` no scope da action:
|
|
962
962
|
|
|
963
963
|
```typescript
|
|
964
964
|
import { createInvokerMiddleware } from "@statedelta-actions/rules";
|
|
@@ -969,7 +969,7 @@ const ruleEngine = createRuleEngine({
|
|
|
969
969
|
});
|
|
970
970
|
```
|
|
971
971
|
|
|
972
|
-
###
|
|
972
|
+
### O que ele injeta
|
|
973
973
|
|
|
974
974
|
```typescript
|
|
975
975
|
{
|
|
@@ -981,11 +981,11 @@ const ruleEngine = createRuleEngine({
|
|
|
981
981
|
}
|
|
982
982
|
```
|
|
983
983
|
|
|
984
|
-
###
|
|
984
|
+
### Propagação de scope
|
|
985
985
|
|
|
986
|
-
`$invoker`
|
|
986
|
+
`$invoker` propaga para todas as sub-actions no ActionEngine via scope de prototype chain. Se o seu handler invoca uma action filha, o `frame.scope.$invoker` da filha herda do pai.
|
|
987
987
|
|
|
988
|
-
###
|
|
988
|
+
### Caso de uso: trilha de auditoria
|
|
989
989
|
|
|
990
990
|
```typescript
|
|
991
991
|
const auditHandler = {
|
|
@@ -1004,17 +1004,17 @@ const auditHandler = {
|
|
|
1004
1004
|
|
|
1005
1005
|
---
|
|
1006
1006
|
|
|
1007
|
-
##
|
|
1007
|
+
## Referência Completa da API
|
|
1008
1008
|
|
|
1009
1009
|
### IRuleEngine\<TCtx\>
|
|
1010
1010
|
|
|
1011
1011
|
```typescript
|
|
1012
1012
|
interface IRuleEngine<TCtx> {
|
|
1013
|
-
//
|
|
1013
|
+
// Registro
|
|
1014
1014
|
register(rules: readonly RuleDefinition<TCtx>[]): RuleRegisterResult;
|
|
1015
1015
|
unregister(id: string): boolean;
|
|
1016
1016
|
|
|
1017
|
-
//
|
|
1017
|
+
// Avaliação de trigger
|
|
1018
1018
|
evaluate(ctx: TCtx): RuleEvaluationResult;
|
|
1019
1019
|
evaluateAsync(ctx: TCtx): Promise<RuleEvaluationResult>;
|
|
1020
1020
|
|
|
@@ -1026,9 +1026,9 @@ interface IRuleEngine<TCtx> {
|
|
|
1026
1026
|
// JIT
|
|
1027
1027
|
compile(): void;
|
|
1028
1028
|
|
|
1029
|
-
//
|
|
1030
|
-
has(id: string): boolean; //
|
|
1031
|
-
readonly size: number; // top-level
|
|
1029
|
+
// Introspecção
|
|
1030
|
+
has(id: string): boolean; // aceita IDs qualificados para sub-rules
|
|
1031
|
+
readonly size: number; // contagem de rules top-level
|
|
1032
1032
|
|
|
1033
1033
|
// Accessors
|
|
1034
1034
|
readonly actionEngine: IActionEngine<TCtx>;
|
|
@@ -1041,12 +1041,12 @@ interface IRuleEngine<TCtx> {
|
|
|
1041
1041
|
|
|
1042
1042
|
```typescript
|
|
1043
1043
|
interface RuleEngineConfig<TCtx> {
|
|
1044
|
-
actionEngine: IActionEngine<TCtx>; //
|
|
1045
|
-
middleware?: readonly RuleMiddleware<TCtx>[]; //
|
|
1046
|
-
ruleHooks?: RuleHooks<TCtx>; //
|
|
1047
|
-
maxSubRuleDepth?: number; //
|
|
1048
|
-
mode?: "interpret" | "jit" | "auto"; //
|
|
1049
|
-
autoJitThreshold?: number; //
|
|
1044
|
+
actionEngine: IActionEngine<TCtx>; // obrigatório
|
|
1045
|
+
middleware?: readonly RuleMiddleware<TCtx>[]; // opcional, default []
|
|
1046
|
+
ruleHooks?: RuleHooks<TCtx>; // opcional
|
|
1047
|
+
maxSubRuleDepth?: number; // opcional, default 10
|
|
1048
|
+
mode?: "interpret" | "jit" | "auto"; // opcional, default "auto"
|
|
1049
|
+
autoJitThreshold?: number; // opcional, default 8
|
|
1050
1050
|
}
|
|
1051
1051
|
```
|
|
1052
1052
|
|
|
@@ -1066,9 +1066,9 @@ interface FrameCounters {
|
|
|
1066
1066
|
|
|
1067
1067
|
---
|
|
1068
1068
|
|
|
1069
|
-
##
|
|
1069
|
+
## Caso de Uso: Tick de Combate RPG
|
|
1070
1070
|
|
|
1071
|
-
|
|
1071
|
+
Um tick de jogo que avalia rules de combate com sub-rules e halt.
|
|
1072
1072
|
|
|
1073
1073
|
```typescript
|
|
1074
1074
|
interface GameCtx {
|
|
@@ -1080,7 +1080,7 @@ interface GameCtx {
|
|
|
1080
1080
|
log: string[];
|
|
1081
1081
|
}
|
|
1082
1082
|
|
|
1083
|
-
// --- ActionEngine
|
|
1083
|
+
// --- Setup do ActionEngine ---
|
|
1084
1084
|
|
|
1085
1085
|
const actionEngine = createActionEngine<GameCtx>({
|
|
1086
1086
|
handlers: {
|
|
@@ -1091,7 +1091,7 @@ const actionEngine = createActionEngine<GameCtx>({
|
|
|
1091
1091
|
},
|
|
1092
1092
|
});
|
|
1093
1093
|
|
|
1094
|
-
// --- RuleEngine
|
|
1094
|
+
// --- Setup do RuleEngine ---
|
|
1095
1095
|
|
|
1096
1096
|
const ruleEngine = createRuleEngine<GameCtx>({
|
|
1097
1097
|
actionEngine,
|
|
@@ -1106,18 +1106,18 @@ const ruleEngine = createRuleEngine<GameCtx>({
|
|
|
1106
1106
|
// --- Rules ---
|
|
1107
1107
|
|
|
1108
1108
|
ruleEngine.register([
|
|
1109
|
-
//
|
|
1109
|
+
// Check de morte de alta priority — para tudo
|
|
1110
1110
|
{
|
|
1111
1111
|
id: "death-check",
|
|
1112
1112
|
priority: 500,
|
|
1113
1113
|
when: (ctx) => ctx.hp <= 0,
|
|
1114
1114
|
then: [
|
|
1115
|
-
{ type: "log", message: "
|
|
1115
|
+
{ type: "log", message: "morto — halting" },
|
|
1116
1116
|
{ type: "halt" },
|
|
1117
1117
|
],
|
|
1118
1118
|
},
|
|
1119
1119
|
|
|
1120
|
-
//
|
|
1120
|
+
// Rules de zona de combate com sub-rules
|
|
1121
1121
|
{
|
|
1122
1122
|
id: "combat-zone",
|
|
1123
1123
|
priority: 100,
|
|
@@ -1137,7 +1137,7 @@ ruleEngine.register([
|
|
|
1137
1137
|
],
|
|
1138
1138
|
},
|
|
1139
1139
|
|
|
1140
|
-
// MP
|
|
1140
|
+
// Regen de MP
|
|
1141
1141
|
{
|
|
1142
1142
|
id: "mp-regen",
|
|
1143
1143
|
priority: 50,
|
|
@@ -1153,16 +1153,16 @@ ruleEngine.register([
|
|
|
1153
1153
|
const ctx: GameCtx = { hp: 40, maxHp: 100, mp: 30, zone: "combat", effects: [], log: [] };
|
|
1154
1154
|
|
|
1155
1155
|
const result = ruleEngine.evaluate(ctx);
|
|
1156
|
-
// combat-zone
|
|
1157
|
-
// low-hp-heal
|
|
1158
|
-
// mp-regen
|
|
1156
|
+
// combat-zone dispara: hp 40->25, emite "damage-tick"
|
|
1157
|
+
// low-hp-heal dispara: hp 25->30
|
|
1158
|
+
// mp-regen dispara: mp 30->33
|
|
1159
1159
|
```
|
|
1160
1160
|
|
|
1161
1161
|
---
|
|
1162
1162
|
|
|
1163
|
-
##
|
|
1163
|
+
## Caso de Uso: Pipeline de Pedido E-commerce
|
|
1164
1164
|
|
|
1165
|
-
|
|
1165
|
+
Um pipeline de processamento de pedido com validação, descontos por tier e detecção de fraude.
|
|
1166
1166
|
|
|
1167
1167
|
```typescript
|
|
1168
1168
|
interface OrderCtx {
|
|
@@ -1180,7 +1180,7 @@ interface OrderCtx {
|
|
|
1180
1180
|
}
|
|
1181
1181
|
|
|
1182
1182
|
ruleEngine.register([
|
|
1183
|
-
// 1.
|
|
1183
|
+
// 1. Detecção de fraude — priority mais alta, para o pipeline
|
|
1184
1184
|
{
|
|
1185
1185
|
id: "fraud-check",
|
|
1186
1186
|
priority: 500,
|
|
@@ -1192,7 +1192,7 @@ ruleEngine.register([
|
|
|
1192
1192
|
],
|
|
1193
1193
|
},
|
|
1194
1194
|
|
|
1195
|
-
// 2.
|
|
1195
|
+
// 2. Calcula o subtotal
|
|
1196
1196
|
{
|
|
1197
1197
|
id: "calc-subtotal",
|
|
1198
1198
|
priority: 400,
|
|
@@ -1208,12 +1208,12 @@ ruleEngine.register([
|
|
|
1208
1208
|
],
|
|
1209
1209
|
},
|
|
1210
1210
|
|
|
1211
|
-
// 3.
|
|
1211
|
+
// 3. Descontos por tier via sub-rules
|
|
1212
1212
|
{
|
|
1213
1213
|
id: "discount-engine",
|
|
1214
1214
|
priority: 300,
|
|
1215
1215
|
when: (ctx) => ctx.status === "validated" && ctx.discount === 0,
|
|
1216
|
-
then: [{ type: "log", message: "
|
|
1216
|
+
then: [{ type: "log", message: "avaliando descontos" }],
|
|
1217
1217
|
rules: [
|
|
1218
1218
|
{
|
|
1219
1219
|
id: "gold-discount",
|
|
@@ -1234,7 +1234,7 @@ ruleEngine.register([
|
|
|
1234
1234
|
],
|
|
1235
1235
|
},
|
|
1236
1236
|
|
|
1237
|
-
// 4.
|
|
1237
|
+
// 4. Pagamento
|
|
1238
1238
|
{
|
|
1239
1239
|
id: "process-payment",
|
|
1240
1240
|
priority: 200,
|
|
@@ -1251,9 +1251,9 @@ ruleEngine.register([
|
|
|
1251
1251
|
|
|
1252
1252
|
---
|
|
1253
1253
|
|
|
1254
|
-
##
|
|
1254
|
+
## Caso de Uso: Workflow de Aprovação de Deploy
|
|
1255
1255
|
|
|
1256
|
-
|
|
1256
|
+
Um pipeline de CI/CD com submissão, atribuição de reviewers, gates de aprovação e seleção de estratégia de deploy via sub-rules.
|
|
1257
1257
|
|
|
1258
1258
|
```typescript
|
|
1259
1259
|
interface WorkflowCtx {
|
|
@@ -1273,7 +1273,7 @@ interface WorkflowCtx {
|
|
|
1273
1273
|
}
|
|
1274
1274
|
|
|
1275
1275
|
ruleEngine.register([
|
|
1276
|
-
// 1.
|
|
1276
|
+
// 1. Check de rejeição — priority mais alta, para tudo
|
|
1277
1277
|
{
|
|
1278
1278
|
id: "check-rejections",
|
|
1279
1279
|
priority: 500,
|
|
@@ -1284,7 +1284,7 @@ ruleEngine.register([
|
|
|
1284
1284
|
],
|
|
1285
1285
|
},
|
|
1286
1286
|
|
|
1287
|
-
// 2.
|
|
1287
|
+
// 2. Submissão com atribuição de reviewers via sub-rules
|
|
1288
1288
|
{
|
|
1289
1289
|
id: "submit",
|
|
1290
1290
|
priority: 400,
|
|
@@ -1312,7 +1312,7 @@ ruleEngine.register([
|
|
|
1312
1312
|
],
|
|
1313
1313
|
},
|
|
1314
1314
|
|
|
1315
|
-
// 3.
|
|
1315
|
+
// 3. Estratégia de deploy por environment + risco via sub-rules
|
|
1316
1316
|
{
|
|
1317
1317
|
id: "deploy",
|
|
1318
1318
|
priority: 100,
|
|
@@ -1322,7 +1322,7 @@ ruleEngine.register([
|
|
|
1322
1322
|
{
|
|
1323
1323
|
id: "deploy-production",
|
|
1324
1324
|
when: (ctx) => ctx.environment === "production",
|
|
1325
|
-
then: [{ type: "log", message: "
|
|
1325
|
+
then: [{ type: "log", message: "deploy de produção" }],
|
|
1326
1326
|
rules: [
|
|
1327
1327
|
{
|
|
1328
1328
|
id: "canary-deploy",
|
|
@@ -1347,31 +1347,31 @@ ruleEngine.register([
|
|
|
1347
1347
|
|
|
1348
1348
|
---
|
|
1349
1349
|
|
|
1350
|
-
## Performance
|
|
1350
|
+
## Espectro de Performance
|
|
1351
1351
|
|
|
1352
1352
|
```
|
|
1353
|
-
Tier 0 (
|
|
1353
|
+
Tier 0 (sem hooks, sem middleware) Full (hooks + middleware + sub-rules)
|
|
1354
1354
|
---------------------------------------------------------------------
|
|
1355
|
-
Zero
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1355
|
+
Zero alocações para hooks evalCtx criado por evaluate()
|
|
1356
|
+
Sem try/catch try/catch por hook
|
|
1357
|
+
Sem variável params params alocados + pipeline de middleware
|
|
1358
|
+
Loop minimal Pipeline completo de governança
|
|
1359
1359
|
```
|
|
1360
1360
|
|
|
1361
|
-
|
|
1361
|
+
O compilador JIT emite apenas os code paths que estão configurados. Tier 0 é o **caso comum** para cenários de alta performance (game loops, tempo real).
|
|
1362
1362
|
|
|
1363
1363
|
---
|
|
1364
1364
|
|
|
1365
|
-
##
|
|
1365
|
+
## Notas de Design Interno
|
|
1366
1366
|
|
|
1367
|
-
|
|
1367
|
+
Para arquitetura interna detalhada, veja [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
|
|
1368
1368
|
|
|
1369
|
-
###
|
|
1369
|
+
### Decisões-chave
|
|
1370
1370
|
|
|
1371
|
-
- **Rules
|
|
1372
|
-
- **Sub-rules
|
|
1373
|
-
- **
|
|
1374
|
-
- **Closure counter + noop
|
|
1375
|
-
- **`Object.assign` in-place**
|
|
1376
|
-
- **
|
|
1377
|
-
- **
|
|
1371
|
+
- **Rules são hidden actions** com prefixo `rule:`. Sub-rules usam separador `.`.
|
|
1372
|
+
- **Sub-rules são sempre interpretadas** — o JIT compila apenas o loop principal.
|
|
1373
|
+
- **Ponto de saída único** no interpretador — `onRulesComplete` chamado exatamente uma vez.
|
|
1374
|
+
- **Closure counter + swap por noop** para auto-promote — zero overhead pós-JIT.
|
|
1375
|
+
- **`Object.assign` in-place** para middleware — 1 alocação vs N+1.
|
|
1376
|
+
- **Imutabilidade por convenção** — sem `Object.freeze()` (custo mensurável em hot paths).
|
|
1377
|
+
- **Eventos são um package separado** — `@statedelta-actions/events`. O RuleEngine não conhece eventos.
|