@statedelta-actions/actions 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Directive, ExecutionFrame, RuleEngineRef, ApplyResult, RegisterWarning, ActionEngineRef, DirectiveResult, HookFn, InterceptResult, AbortDecision, FrameLimits, SlotAnalysis } from '@statedelta-actions/core';
2
2
  export { RegisterWarning } from '@statedelta-actions/core';
3
3
 
4
- type DirectiveHandler<TCtx> = (directive: Directive, frame: ExecutionFrame<TCtx>, engine: RuleEngineRef<TCtx>) => ApplyResult;
4
+ type DirectiveHandler<TCtx> = (directive: Directive, frame: ExecutionFrame<TCtx>, engine: RuleEngineRef<TCtx>) => ApplyResult | Promise<ApplyResult>;
5
5
  type DirectiveHandlerMap<TCtx> = Record<string, DirectiveHandler<TCtx>>;
6
6
  interface HandlerAnalysis {
7
7
  capabilities: string[];
@@ -19,8 +19,44 @@ interface HandlerDefinition<TCtx> {
19
19
  validate?: (directive: Directive) => ValidationResult | void;
20
20
  /** JIT-time: contribui código inline per-action (Fase 2c). */
21
21
  compile?: (directive: Directive, compiler: unknown) => string | void;
22
+ /**
23
+ * Marca o handler como assíncrono (opt-in explícito).
24
+ *
25
+ * Quando true (ou quando `execute` é uma `async function`), o engine:
26
+ * - inclui este handler no async handler set;
27
+ * - força `isAsync = true` no engine inteiro;
28
+ * - emite `await` ao invocar este handler no interpreter async e nos JITs.
29
+ *
30
+ * Auto-detecção via `isAsyncFunction(execute)` cobre `async function`.
31
+ * Use a flag para wrappers que retornam `Promise<ApplyResult>` sem
32
+ * serem `async function` (ex: factory que retorna função normal que
33
+ * delega pra um await internamente via `.then`).
34
+ *
35
+ * Sem flag e sem `async function`: handler é tratado como sync.
36
+ */
37
+ async?: boolean;
38
+ /**
39
+ * Marca o handler como interativo (opt-in explícito).
40
+ *
41
+ * Quando true (ou quando `execute` é uma `function*` / `async function*`),
42
+ * o engine:
43
+ * - inclui este handler no interactive handler set;
44
+ * - propaga transitividade `_interactiveActions` no mini-graph;
45
+ * - emite `yield* handler(...)` ao invocar nas actions interactive;
46
+ * - exige `engine.invokeInteractive()` em actions cuja sub-árvore use o handler.
47
+ *
48
+ * Auto-detecção via `isGeneratorFunction(execute)` cobre `function*` e
49
+ * `async function*`. Use a flag para wrappers que retornam iterator sem
50
+ * serem generator function.
51
+ *
52
+ * Handler com `interactive: true` em engine sem `interactive` config →
53
+ * erro no constructor (fail-fast).
54
+ *
55
+ * Sem flag e sem generator function: handler é tratado como regular.
56
+ */
57
+ interactive?: boolean;
22
58
  /** Runtime: executa a directive (obrigatório). */
23
- execute: (directive: Directive, frame: ExecutionFrame<TCtx>, engine: ActionEngineRef<TCtx>) => ApplyResult;
59
+ execute: (directive: Directive, frame: ExecutionFrame<TCtx>, engine: ActionEngineRef<TCtx>) => ApplyResult | Promise<ApplyResult>;
24
60
  }
25
61
  /**
26
62
  * Aceita handler V1 (função) ou V2 (objeto com fases).
@@ -54,6 +90,20 @@ interface ActionDefinition<TCtx = unknown> {
54
90
  * Requer tierPropagator habilitado no grafo.
55
91
  */
56
92
  tier?: number;
93
+ /**
94
+ * Interactive — declaração explícita de que a action é interativa.
95
+ *
96
+ * Quando true, força `actionIsInteractive(id) === true` no mini-graph
97
+ * mesmo que a inferência local não detecte (ex: handler `action`
98
+ * dinâmico que pode invocar interactive em runtime).
99
+ *
100
+ * Pode ser inferida (sub-árvore usa handler interactive ou type:"pause").
101
+ * Quando declarada, o analyzer (opt-in) valida contra a inferência:
102
+ * declared !== inferred → DECLARATION_CONFLICT.
103
+ *
104
+ * Engine respeita declaração na propagação any_true.
105
+ */
106
+ interactive?: boolean;
57
107
  /**
58
108
  * Declarações — contratos públicos (trust boundaries).
59
109
  * Cada key = nome de um propagator registrado no grafo.
@@ -122,6 +172,75 @@ interface DirectivePermissionConfig {
122
172
  */
123
173
  type DirectivePermissionEntry = string | DirectivePermissionConfig;
124
174
 
175
+ /**
176
+ * Configuração do modo interactive.
177
+ *
178
+ * v0: estrutura mínima — habilita o modo. Controller dinâmico
179
+ * (`shouldPause`) pra debug arbitrário fica reservado pra evolução
180
+ * futura.
181
+ *
182
+ * Habilitar `interactive: {}` ativa:
183
+ * - mini-graph propaga `_interactiveActions` transitivo
184
+ * - actions cuja sub-árvore tem handler interactive ou `type:"pause"`
185
+ * compilam como generator (na fase JIT correspondente)
186
+ * - `engine.invoke()` lança per-action transitivamente
187
+ * - `engine.invokeInteractive()` retorna iterator
188
+ *
189
+ * Sem `interactive` configurado:
190
+ * - handler com `interactive: true` → erro no constructor
191
+ * - diretiva `type: "pause"` → erro no register
192
+ */
193
+ interface InteractiveConfig {
194
+ }
195
+ /**
196
+ * Evento yieldado durante execução de uma action interactive.
197
+ *
198
+ * - `source: "handler"` — handler interactive yieldou. `payload` é
199
+ * opaco (handler controla o protocol).
200
+ * - `source: "pause"` — diretiva `type:"pause"` engine-level. `payload`
201
+ * contém `{ message?: unknown }` extraído da diretiva.
202
+ */
203
+ interface PauseEvent {
204
+ readonly source: "handler" | "pause";
205
+ readonly directive: Directive;
206
+ readonly frame: ExecutionFrame<unknown>;
207
+ readonly index: number;
208
+ readonly payload?: unknown;
209
+ }
210
+ /**
211
+ * Sessão interativa retornada por `engine.invokeInteractive()` em
212
+ * actions sync (sem handler async na sub-árvore).
213
+ *
214
+ * Drena via `next()`/`return()`/`throw()` ou `for...of`.
215
+ */
216
+ interface InteractiveSession extends Iterator<PauseEvent, DirectiveResult, unknown> {
217
+ next(value?: unknown): IteratorResult<PauseEvent, DirectiveResult>;
218
+ return(): IteratorResult<PauseEvent, DirectiveResult>;
219
+ throw(err?: unknown): IteratorResult<PauseEvent, DirectiveResult>;
220
+ [Symbol.iterator](): InteractiveSession;
221
+ }
222
+ /**
223
+ * Sessão interativa async — engine async (handler async transitivo)
224
+ * combinado com interactive.
225
+ *
226
+ * Drena via `await session.next()` / `for await...of`.
227
+ */
228
+ interface AsyncInteractiveSession extends AsyncIterator<PauseEvent, DirectiveResult, unknown> {
229
+ next(value?: unknown): Promise<IteratorResult<PauseEvent, DirectiveResult>>;
230
+ return(): Promise<IteratorResult<PauseEvent, DirectiveResult>>;
231
+ throw(err?: unknown): Promise<IteratorResult<PauseEvent, DirectiveResult>>;
232
+ [Symbol.asyncIterator](): AsyncInteractiveSession;
233
+ }
234
+ /**
235
+ * Forma alternativa de `ApplyResult` que um handler `action` pode
236
+ * retornar quando o target é interactive — engine detecta `iterator`
237
+ * e emite `yield* iterator` no JIT.
238
+ */
239
+ interface InteractiveApplyResult {
240
+ readonly ok: true;
241
+ readonly iterator: Iterator<PauseEvent, ApplyResult, unknown> | AsyncIterator<PauseEvent, ApplyResult, unknown>;
242
+ }
243
+
125
244
  interface DirectiveHooks<TCtx> {
126
245
  beforeDirective?: HookFn<[
127
246
  directive: Directive,
@@ -197,6 +316,20 @@ interface IActionEngineConfig<TCtx> {
197
316
  * Diretivas estáticas (const/let/return/throw) são sempre permitidas.
198
317
  */
199
318
  blockedDirectives?: readonly DirectivePermissionEntry[];
319
+ /**
320
+ * Habilita modo interactive (generator-based pause/resume).
321
+ *
322
+ * Sem essa config:
323
+ * - handler com `interactive: true` → erro no constructor
324
+ * - diretiva `type: "pause"` em alguma action → erro no register
325
+ * - `engine.invokeInteractive()` lança
326
+ *
327
+ * Com `interactive: {}` ativo:
328
+ * - mini-graph propaga `_interactiveActions` transitivo
329
+ * - `engine.invoke()` lança per-action transitivo
330
+ * - `engine.invokeInteractive()` retorna iterator
331
+ */
332
+ interactive?: InteractiveConfig;
200
333
  }
201
334
  interface IActionEngine<TCtx> {
202
335
  /** Registra actions. Valida, analisa, indexa no grafo, compila. */
@@ -235,12 +368,98 @@ interface IActionEngine<TCtx> {
235
368
  */
236
369
  readonly directivePermissions: ReadonlyMap<string, DirectivePermission>;
237
370
  readonly isAsync: boolean;
371
+ readonly isInteractive: boolean;
238
372
  readonly compilationMode: "interpret" | "jit";
239
373
  compile(): void;
240
374
  readonly directiveHookSlots: ReadonlySet<string>;
241
375
  readonly asyncSlots: ReadonlySet<string>;
376
+ /**
377
+ * Action é async transitivamente?
378
+ * Consulta closure `_asyncActions` do mini-graph.
379
+ * `undefined` se id não está registrado.
380
+ */
381
+ isActionAsync(id: string): boolean | undefined;
382
+ /**
383
+ * Action é interactive transitivamente?
384
+ * Consulta closure `_interactiveActions` do mini-graph.
385
+ * `undefined` se id não está registrado.
386
+ */
387
+ isActionInteractive(id: string): boolean | undefined;
388
+ /**
389
+ * Invoca action em modo interactive — retorna iterator drenável.
390
+ * Lança se a action não é interactive ou se engine não tem
391
+ * `interactive` configurado.
392
+ *
393
+ * Variante async (AsyncInteractiveSession) é retornada quando a
394
+ * action é async transitivamente.
395
+ */
396
+ invokeInteractive(id: string, params?: Record<string, unknown>, ctx?: TCtx): InteractiveSession | AsyncInteractiveSession;
242
397
  }
243
398
 
399
+ /**
400
+ * Helpers opcionais pra drenar `InteractiveSession` / `AsyncInteractiveSession`.
401
+ *
402
+ * `engine.invokeInteractive()` retorna iterator. Em "play mode" (sem debug),
403
+ * o consumer só quer o resultado final — drenar o iterator manualmente é
404
+ * verboso. Esses helpers fazem o trabalho.
405
+ *
406
+ * Em modo debug ou UX interativa real, o consumer dirige `next()` direto.
407
+ */
408
+ /**
409
+ * Função do consumer pra responder a um yield do iterator. Retorna o valor
410
+ * que o iterator vai receber via `next(value)`.
411
+ *
412
+ * O `event` pode ser:
413
+ * - `PauseEvent` (engine-level — `type:"pause"`): tem `source`, `directive`,
414
+ * `frame`, `index`, `payload: { message }`.
415
+ * - **Qualquer coisa** (handler interactive): handler controla 100% o
416
+ * payload. Pode ser `{ prompt, options }`, `{ kind, data }`, etc.
417
+ *
418
+ * Retornos comuns:
419
+ * - `undefined` ou ausente → continua / captura `undefined` em `as`
420
+ * - `false` / `"abort"` / `"cancel"` → aborta `type:"pause"`
421
+ * - qualquer dado → entregue ao handler ou capturado em `as`
422
+ */
423
+ type Responder = (event: PauseEvent | unknown) => unknown;
424
+ /**
425
+ * Drena uma `InteractiveSession` síncrona. Chama `next(value)` em loop até
426
+ * `done`, opcionalmente consultando `responder` em cada pausa.
427
+ *
428
+ * Sem responder: cada pausa recebe `undefined` (continua/captura `undefined`
429
+ * em `as`). Útil pra "play through" em testes determinísticos.
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * const session = engine.invokeInteractive("foo", undefined, ctx) as InteractiveSession;
434
+ * const result = drainSync(session, (ev) => {
435
+ * if (ev.source === "pause") return "ok";
436
+ * if (ev.payload?.prompt === "Nome?") return "Anderson";
437
+ * return undefined;
438
+ * });
439
+ * ```
440
+ */
441
+ declare function drainSync(session: InteractiveSession, responder?: Responder): DirectiveResult;
442
+ /**
443
+ * Drena uma `AsyncInteractiveSession`. Versão async de `drainSync`.
444
+ *
445
+ * Suporta responder sync ou async — se retornar Promise, é awaited.
446
+ */
447
+ declare function drainAsync(session: AsyncInteractiveSession, responder?: Responder): Promise<DirectiveResult>;
448
+ /**
449
+ * Cria um responder pré-programado a partir de uma lista de respostas.
450
+ * Cada pausa consome a próxima resposta na ordem. Ideal pra testes.
451
+ *
452
+ * @example
453
+ * ```ts
454
+ * const responder = replayResponder(["Anderson", "yes", 42]);
455
+ * const result = drainSync(session, responder);
456
+ * ```
457
+ *
458
+ * Se o iterator pausar mais vezes que a lista tem respostas, retorna
459
+ * `undefined` pras pausas extras (sem erro).
460
+ */
461
+ declare function replayResponder(responses: readonly unknown[]): Responder;
462
+
244
463
  /**
245
464
  * Type-safe event emitter. Zero deps. Browser-compatible.
246
465
  *
@@ -286,7 +505,7 @@ declare class SimpleEmitter<TEvents = Record<string, unknown>> {
286
505
  }
287
506
 
288
507
  declare const RESERVED_TYPES: Set<string>;
289
- declare function normalizeDirectives<TCtx>(directives: readonly Directive<TCtx>[], _typeField: string): Directive<TCtx>[];
508
+ declare function normalizeDirectives<TCtx>(directives: readonly Directive<TCtx>[], typeField: string): Directive<TCtx>[];
290
509
 
291
510
  declare function createDirectiveInterpreter<TCtx>(analysis: SlotAnalysis, isAsync: boolean): DirectiveExecutorFn<TCtx>;
292
511
 
@@ -297,13 +516,53 @@ interface GeneratedDirectiveExecutor {
297
516
  fn: RuntimeDirectiveExecutorFn;
298
517
  isAsync: boolean;
299
518
  }
300
- declare function buildDirectiveExecutor(analysis: SlotAnalysis): GeneratedDirectiveExecutor;
519
+ /**
520
+ * Compila um executor genérico de diretivas via `new Function`.
521
+ *
522
+ * @param analysis — slot analysis dos directive hooks (filled/async).
523
+ * @param isAsync — se true, gera função `async ()` e prefixa `await` na chamada
524
+ * do handler. Default: `analysis.hasAnyAsync` (preserva o comportamento legado
525
+ * pra callers que não conhecem handler async). O engine sempre passa explícito,
526
+ * computado a partir de `directiveAnalysis.hasAnyAsync || asyncHandlerSet.size > 0`.
527
+ */
528
+ declare function buildDirectiveExecutor(analysis: SlotAnalysis, isAsync?: boolean): GeneratedDirectiveExecutor;
301
529
 
302
530
  type RuntimeActionExecutorFn = (frame: unknown, scope: unknown, $: unknown) => unknown;
303
531
  interface GeneratedActionExecutor {
304
532
  fn: RuntimeActionExecutorFn;
533
+ /** Wrapper async (`async function`/`async function*`)? */
305
534
  isAsync: boolean;
535
+ /** Wrapper generator (`function*`/`async function*`)? Quando true, `fn(...)` retorna Generator/AsyncGenerator. */
536
+ isInteractive: boolean;
306
537
  }
307
- declare function buildActionExecutor(directives: readonly Directive[], analysis: SlotAnalysis, typeField: string): GeneratedActionExecutor;
538
+ /**
539
+ * Compila um executor unrolled per-action via `new Function`.
540
+ *
541
+ * Decisão sync/async é **por action** (ADR-021 refinado):
542
+ * - Action com pelo menos 1 handler async (transitivamente, via mini-graph)
543
+ * ou hook async → wrapper `async ()`, `await` em todos os handlers.
544
+ * - Action 100% sync na sub-árvore inteira → wrapper sync `()`, zero `await`.
545
+ *
546
+ * Em um engine híbrido (alguns handlers async, outros sync), actions cuja
547
+ * sub-árvore inteira é sync não pagam o overhead de async functions.
548
+ *
549
+ * @param directives — diretivas normalizadas da action.
550
+ * @param analysis — slot analysis dos directive hooks (filled/async).
551
+ * @param typeField — campo de dispatch (default: "type").
552
+ * @param asyncHandlerSet — conjunto de tipos de handler marcados como async
553
+ * (via flag `async: true` ou auto-detect `isAsyncFunction`). Se omitido,
554
+ * nenhum handler é considerado async (compat backward).
555
+ * @param actionUsesAsyncOverride — se fornecido, substitui o cálculo local
556
+ * `directives.some(asyncHandlerSet.has)`. Usado pelo engine pra passar a
557
+ * info **transitiva** computada pelo mini-graph (ADR-026): action que
558
+ * invoca outra action async via `{ type: "action", id: X }` é async
559
+ * transitivamente, mas `asyncHandlerSet.has("action")` retorna false
560
+ * (handler `action` em si não é async). Sem essa override, JIT compilaria
561
+ * wrapper sync pra action async transitiva — bug.
562
+ *
563
+ * Hooks async (`hasAnyAsync`) sempre forçam wrapper async (independente
564
+ * do override) — wrapper sync não pode fazer await em hook.
565
+ */
566
+ declare function buildActionExecutor(directives: readonly Directive[], analysis: SlotAnalysis, typeField: string, asyncHandlerSet?: ReadonlySet<string>, actionUsesAsyncOverride?: boolean, actionIsInteractive?: boolean, interactiveHandlerSet?: ReadonlySet<string>): GeneratedActionExecutor;
308
567
 
309
- export { type ActionDefinition, type ActionHooks, type ActionInterceptResult, type DirectiveExecutorFn, type DirectiveHandler, type DirectiveHandlerMap, type DirectiveHooks, type DirectivePermission, type DirectivePermissionConfig, type DirectivePermissionEntry, type DirectivePermissionStatus, type DirectiveRunnerFn, type EngineEventMap, type EngineEventName, type GeneratedActionExecutor, type GeneratedDirectiveExecutor, type HandlerAnalysis, type HandlerDefinition, type HandlerInput, type HandlerInputMap, type IActionEngine, type IActionEngineConfig, type Listener, RESERVED_TYPES, type RegisterError, type RegisterEvent, type RegisterResult, SimpleEmitter, type UnregisterEvent, type ValidationResult, buildActionExecutor, buildDirectiveExecutor, createActionEngine, createDirectiveInterpreter, normalizeDirectives };
568
+ export { type ActionDefinition, type ActionHooks, type ActionInterceptResult, type AsyncInteractiveSession, type DirectiveExecutorFn, type DirectiveHandler, type DirectiveHandlerMap, type DirectiveHooks, type DirectivePermission, type DirectivePermissionConfig, type DirectivePermissionEntry, type DirectivePermissionStatus, type DirectiveRunnerFn, type EngineEventMap, type EngineEventName, type GeneratedActionExecutor, type GeneratedDirectiveExecutor, type HandlerAnalysis, type HandlerDefinition, type HandlerInput, type HandlerInputMap, type IActionEngine, type IActionEngineConfig, type InteractiveApplyResult, type InteractiveConfig, type InteractiveSession, type Listener, type PauseEvent, RESERVED_TYPES, type RegisterError, type RegisterEvent, type RegisterResult, type Responder, SimpleEmitter, type UnregisterEvent, type ValidationResult, buildActionExecutor, buildDirectiveExecutor, createActionEngine, createDirectiveInterpreter, drainAsync, drainSync, normalizeDirectives, replayResponder };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Directive, ExecutionFrame, RuleEngineRef, ApplyResult, RegisterWarning, ActionEngineRef, DirectiveResult, HookFn, InterceptResult, AbortDecision, FrameLimits, SlotAnalysis } from '@statedelta-actions/core';
2
2
  export { RegisterWarning } from '@statedelta-actions/core';
3
3
 
4
- type DirectiveHandler<TCtx> = (directive: Directive, frame: ExecutionFrame<TCtx>, engine: RuleEngineRef<TCtx>) => ApplyResult;
4
+ type DirectiveHandler<TCtx> = (directive: Directive, frame: ExecutionFrame<TCtx>, engine: RuleEngineRef<TCtx>) => ApplyResult | Promise<ApplyResult>;
5
5
  type DirectiveHandlerMap<TCtx> = Record<string, DirectiveHandler<TCtx>>;
6
6
  interface HandlerAnalysis {
7
7
  capabilities: string[];
@@ -19,8 +19,44 @@ interface HandlerDefinition<TCtx> {
19
19
  validate?: (directive: Directive) => ValidationResult | void;
20
20
  /** JIT-time: contribui código inline per-action (Fase 2c). */
21
21
  compile?: (directive: Directive, compiler: unknown) => string | void;
22
+ /**
23
+ * Marca o handler como assíncrono (opt-in explícito).
24
+ *
25
+ * Quando true (ou quando `execute` é uma `async function`), o engine:
26
+ * - inclui este handler no async handler set;
27
+ * - força `isAsync = true` no engine inteiro;
28
+ * - emite `await` ao invocar este handler no interpreter async e nos JITs.
29
+ *
30
+ * Auto-detecção via `isAsyncFunction(execute)` cobre `async function`.
31
+ * Use a flag para wrappers que retornam `Promise<ApplyResult>` sem
32
+ * serem `async function` (ex: factory que retorna função normal que
33
+ * delega pra um await internamente via `.then`).
34
+ *
35
+ * Sem flag e sem `async function`: handler é tratado como sync.
36
+ */
37
+ async?: boolean;
38
+ /**
39
+ * Marca o handler como interativo (opt-in explícito).
40
+ *
41
+ * Quando true (ou quando `execute` é uma `function*` / `async function*`),
42
+ * o engine:
43
+ * - inclui este handler no interactive handler set;
44
+ * - propaga transitividade `_interactiveActions` no mini-graph;
45
+ * - emite `yield* handler(...)` ao invocar nas actions interactive;
46
+ * - exige `engine.invokeInteractive()` em actions cuja sub-árvore use o handler.
47
+ *
48
+ * Auto-detecção via `isGeneratorFunction(execute)` cobre `function*` e
49
+ * `async function*`. Use a flag para wrappers que retornam iterator sem
50
+ * serem generator function.
51
+ *
52
+ * Handler com `interactive: true` em engine sem `interactive` config →
53
+ * erro no constructor (fail-fast).
54
+ *
55
+ * Sem flag e sem generator function: handler é tratado como regular.
56
+ */
57
+ interactive?: boolean;
22
58
  /** Runtime: executa a directive (obrigatório). */
23
- execute: (directive: Directive, frame: ExecutionFrame<TCtx>, engine: ActionEngineRef<TCtx>) => ApplyResult;
59
+ execute: (directive: Directive, frame: ExecutionFrame<TCtx>, engine: ActionEngineRef<TCtx>) => ApplyResult | Promise<ApplyResult>;
24
60
  }
25
61
  /**
26
62
  * Aceita handler V1 (função) ou V2 (objeto com fases).
@@ -54,6 +90,20 @@ interface ActionDefinition<TCtx = unknown> {
54
90
  * Requer tierPropagator habilitado no grafo.
55
91
  */
56
92
  tier?: number;
93
+ /**
94
+ * Interactive — declaração explícita de que a action é interativa.
95
+ *
96
+ * Quando true, força `actionIsInteractive(id) === true` no mini-graph
97
+ * mesmo que a inferência local não detecte (ex: handler `action`
98
+ * dinâmico que pode invocar interactive em runtime).
99
+ *
100
+ * Pode ser inferida (sub-árvore usa handler interactive ou type:"pause").
101
+ * Quando declarada, o analyzer (opt-in) valida contra a inferência:
102
+ * declared !== inferred → DECLARATION_CONFLICT.
103
+ *
104
+ * Engine respeita declaração na propagação any_true.
105
+ */
106
+ interactive?: boolean;
57
107
  /**
58
108
  * Declarações — contratos públicos (trust boundaries).
59
109
  * Cada key = nome de um propagator registrado no grafo.
@@ -122,6 +172,75 @@ interface DirectivePermissionConfig {
122
172
  */
123
173
  type DirectivePermissionEntry = string | DirectivePermissionConfig;
124
174
 
175
+ /**
176
+ * Configuração do modo interactive.
177
+ *
178
+ * v0: estrutura mínima — habilita o modo. Controller dinâmico
179
+ * (`shouldPause`) pra debug arbitrário fica reservado pra evolução
180
+ * futura.
181
+ *
182
+ * Habilitar `interactive: {}` ativa:
183
+ * - mini-graph propaga `_interactiveActions` transitivo
184
+ * - actions cuja sub-árvore tem handler interactive ou `type:"pause"`
185
+ * compilam como generator (na fase JIT correspondente)
186
+ * - `engine.invoke()` lança per-action transitivamente
187
+ * - `engine.invokeInteractive()` retorna iterator
188
+ *
189
+ * Sem `interactive` configurado:
190
+ * - handler com `interactive: true` → erro no constructor
191
+ * - diretiva `type: "pause"` → erro no register
192
+ */
193
+ interface InteractiveConfig {
194
+ }
195
+ /**
196
+ * Evento yieldado durante execução de uma action interactive.
197
+ *
198
+ * - `source: "handler"` — handler interactive yieldou. `payload` é
199
+ * opaco (handler controla o protocol).
200
+ * - `source: "pause"` — diretiva `type:"pause"` engine-level. `payload`
201
+ * contém `{ message?: unknown }` extraído da diretiva.
202
+ */
203
+ interface PauseEvent {
204
+ readonly source: "handler" | "pause";
205
+ readonly directive: Directive;
206
+ readonly frame: ExecutionFrame<unknown>;
207
+ readonly index: number;
208
+ readonly payload?: unknown;
209
+ }
210
+ /**
211
+ * Sessão interativa retornada por `engine.invokeInteractive()` em
212
+ * actions sync (sem handler async na sub-árvore).
213
+ *
214
+ * Drena via `next()`/`return()`/`throw()` ou `for...of`.
215
+ */
216
+ interface InteractiveSession extends Iterator<PauseEvent, DirectiveResult, unknown> {
217
+ next(value?: unknown): IteratorResult<PauseEvent, DirectiveResult>;
218
+ return(): IteratorResult<PauseEvent, DirectiveResult>;
219
+ throw(err?: unknown): IteratorResult<PauseEvent, DirectiveResult>;
220
+ [Symbol.iterator](): InteractiveSession;
221
+ }
222
+ /**
223
+ * Sessão interativa async — engine async (handler async transitivo)
224
+ * combinado com interactive.
225
+ *
226
+ * Drena via `await session.next()` / `for await...of`.
227
+ */
228
+ interface AsyncInteractiveSession extends AsyncIterator<PauseEvent, DirectiveResult, unknown> {
229
+ next(value?: unknown): Promise<IteratorResult<PauseEvent, DirectiveResult>>;
230
+ return(): Promise<IteratorResult<PauseEvent, DirectiveResult>>;
231
+ throw(err?: unknown): Promise<IteratorResult<PauseEvent, DirectiveResult>>;
232
+ [Symbol.asyncIterator](): AsyncInteractiveSession;
233
+ }
234
+ /**
235
+ * Forma alternativa de `ApplyResult` que um handler `action` pode
236
+ * retornar quando o target é interactive — engine detecta `iterator`
237
+ * e emite `yield* iterator` no JIT.
238
+ */
239
+ interface InteractiveApplyResult {
240
+ readonly ok: true;
241
+ readonly iterator: Iterator<PauseEvent, ApplyResult, unknown> | AsyncIterator<PauseEvent, ApplyResult, unknown>;
242
+ }
243
+
125
244
  interface DirectiveHooks<TCtx> {
126
245
  beforeDirective?: HookFn<[
127
246
  directive: Directive,
@@ -197,6 +316,20 @@ interface IActionEngineConfig<TCtx> {
197
316
  * Diretivas estáticas (const/let/return/throw) são sempre permitidas.
198
317
  */
199
318
  blockedDirectives?: readonly DirectivePermissionEntry[];
319
+ /**
320
+ * Habilita modo interactive (generator-based pause/resume).
321
+ *
322
+ * Sem essa config:
323
+ * - handler com `interactive: true` → erro no constructor
324
+ * - diretiva `type: "pause"` em alguma action → erro no register
325
+ * - `engine.invokeInteractive()` lança
326
+ *
327
+ * Com `interactive: {}` ativo:
328
+ * - mini-graph propaga `_interactiveActions` transitivo
329
+ * - `engine.invoke()` lança per-action transitivo
330
+ * - `engine.invokeInteractive()` retorna iterator
331
+ */
332
+ interactive?: InteractiveConfig;
200
333
  }
201
334
  interface IActionEngine<TCtx> {
202
335
  /** Registra actions. Valida, analisa, indexa no grafo, compila. */
@@ -235,12 +368,98 @@ interface IActionEngine<TCtx> {
235
368
  */
236
369
  readonly directivePermissions: ReadonlyMap<string, DirectivePermission>;
237
370
  readonly isAsync: boolean;
371
+ readonly isInteractive: boolean;
238
372
  readonly compilationMode: "interpret" | "jit";
239
373
  compile(): void;
240
374
  readonly directiveHookSlots: ReadonlySet<string>;
241
375
  readonly asyncSlots: ReadonlySet<string>;
376
+ /**
377
+ * Action é async transitivamente?
378
+ * Consulta closure `_asyncActions` do mini-graph.
379
+ * `undefined` se id não está registrado.
380
+ */
381
+ isActionAsync(id: string): boolean | undefined;
382
+ /**
383
+ * Action é interactive transitivamente?
384
+ * Consulta closure `_interactiveActions` do mini-graph.
385
+ * `undefined` se id não está registrado.
386
+ */
387
+ isActionInteractive(id: string): boolean | undefined;
388
+ /**
389
+ * Invoca action em modo interactive — retorna iterator drenável.
390
+ * Lança se a action não é interactive ou se engine não tem
391
+ * `interactive` configurado.
392
+ *
393
+ * Variante async (AsyncInteractiveSession) é retornada quando a
394
+ * action é async transitivamente.
395
+ */
396
+ invokeInteractive(id: string, params?: Record<string, unknown>, ctx?: TCtx): InteractiveSession | AsyncInteractiveSession;
242
397
  }
243
398
 
399
+ /**
400
+ * Helpers opcionais pra drenar `InteractiveSession` / `AsyncInteractiveSession`.
401
+ *
402
+ * `engine.invokeInteractive()` retorna iterator. Em "play mode" (sem debug),
403
+ * o consumer só quer o resultado final — drenar o iterator manualmente é
404
+ * verboso. Esses helpers fazem o trabalho.
405
+ *
406
+ * Em modo debug ou UX interativa real, o consumer dirige `next()` direto.
407
+ */
408
+ /**
409
+ * Função do consumer pra responder a um yield do iterator. Retorna o valor
410
+ * que o iterator vai receber via `next(value)`.
411
+ *
412
+ * O `event` pode ser:
413
+ * - `PauseEvent` (engine-level — `type:"pause"`): tem `source`, `directive`,
414
+ * `frame`, `index`, `payload: { message }`.
415
+ * - **Qualquer coisa** (handler interactive): handler controla 100% o
416
+ * payload. Pode ser `{ prompt, options }`, `{ kind, data }`, etc.
417
+ *
418
+ * Retornos comuns:
419
+ * - `undefined` ou ausente → continua / captura `undefined` em `as`
420
+ * - `false` / `"abort"` / `"cancel"` → aborta `type:"pause"`
421
+ * - qualquer dado → entregue ao handler ou capturado em `as`
422
+ */
423
+ type Responder = (event: PauseEvent | unknown) => unknown;
424
+ /**
425
+ * Drena uma `InteractiveSession` síncrona. Chama `next(value)` em loop até
426
+ * `done`, opcionalmente consultando `responder` em cada pausa.
427
+ *
428
+ * Sem responder: cada pausa recebe `undefined` (continua/captura `undefined`
429
+ * em `as`). Útil pra "play through" em testes determinísticos.
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * const session = engine.invokeInteractive("foo", undefined, ctx) as InteractiveSession;
434
+ * const result = drainSync(session, (ev) => {
435
+ * if (ev.source === "pause") return "ok";
436
+ * if (ev.payload?.prompt === "Nome?") return "Anderson";
437
+ * return undefined;
438
+ * });
439
+ * ```
440
+ */
441
+ declare function drainSync(session: InteractiveSession, responder?: Responder): DirectiveResult;
442
+ /**
443
+ * Drena uma `AsyncInteractiveSession`. Versão async de `drainSync`.
444
+ *
445
+ * Suporta responder sync ou async — se retornar Promise, é awaited.
446
+ */
447
+ declare function drainAsync(session: AsyncInteractiveSession, responder?: Responder): Promise<DirectiveResult>;
448
+ /**
449
+ * Cria um responder pré-programado a partir de uma lista de respostas.
450
+ * Cada pausa consome a próxima resposta na ordem. Ideal pra testes.
451
+ *
452
+ * @example
453
+ * ```ts
454
+ * const responder = replayResponder(["Anderson", "yes", 42]);
455
+ * const result = drainSync(session, responder);
456
+ * ```
457
+ *
458
+ * Se o iterator pausar mais vezes que a lista tem respostas, retorna
459
+ * `undefined` pras pausas extras (sem erro).
460
+ */
461
+ declare function replayResponder(responses: readonly unknown[]): Responder;
462
+
244
463
  /**
245
464
  * Type-safe event emitter. Zero deps. Browser-compatible.
246
465
  *
@@ -286,7 +505,7 @@ declare class SimpleEmitter<TEvents = Record<string, unknown>> {
286
505
  }
287
506
 
288
507
  declare const RESERVED_TYPES: Set<string>;
289
- declare function normalizeDirectives<TCtx>(directives: readonly Directive<TCtx>[], _typeField: string): Directive<TCtx>[];
508
+ declare function normalizeDirectives<TCtx>(directives: readonly Directive<TCtx>[], typeField: string): Directive<TCtx>[];
290
509
 
291
510
  declare function createDirectiveInterpreter<TCtx>(analysis: SlotAnalysis, isAsync: boolean): DirectiveExecutorFn<TCtx>;
292
511
 
@@ -297,13 +516,53 @@ interface GeneratedDirectiveExecutor {
297
516
  fn: RuntimeDirectiveExecutorFn;
298
517
  isAsync: boolean;
299
518
  }
300
- declare function buildDirectiveExecutor(analysis: SlotAnalysis): GeneratedDirectiveExecutor;
519
+ /**
520
+ * Compila um executor genérico de diretivas via `new Function`.
521
+ *
522
+ * @param analysis — slot analysis dos directive hooks (filled/async).
523
+ * @param isAsync — se true, gera função `async ()` e prefixa `await` na chamada
524
+ * do handler. Default: `analysis.hasAnyAsync` (preserva o comportamento legado
525
+ * pra callers que não conhecem handler async). O engine sempre passa explícito,
526
+ * computado a partir de `directiveAnalysis.hasAnyAsync || asyncHandlerSet.size > 0`.
527
+ */
528
+ declare function buildDirectiveExecutor(analysis: SlotAnalysis, isAsync?: boolean): GeneratedDirectiveExecutor;
301
529
 
302
530
  type RuntimeActionExecutorFn = (frame: unknown, scope: unknown, $: unknown) => unknown;
303
531
  interface GeneratedActionExecutor {
304
532
  fn: RuntimeActionExecutorFn;
533
+ /** Wrapper async (`async function`/`async function*`)? */
305
534
  isAsync: boolean;
535
+ /** Wrapper generator (`function*`/`async function*`)? Quando true, `fn(...)` retorna Generator/AsyncGenerator. */
536
+ isInteractive: boolean;
306
537
  }
307
- declare function buildActionExecutor(directives: readonly Directive[], analysis: SlotAnalysis, typeField: string): GeneratedActionExecutor;
538
+ /**
539
+ * Compila um executor unrolled per-action via `new Function`.
540
+ *
541
+ * Decisão sync/async é **por action** (ADR-021 refinado):
542
+ * - Action com pelo menos 1 handler async (transitivamente, via mini-graph)
543
+ * ou hook async → wrapper `async ()`, `await` em todos os handlers.
544
+ * - Action 100% sync na sub-árvore inteira → wrapper sync `()`, zero `await`.
545
+ *
546
+ * Em um engine híbrido (alguns handlers async, outros sync), actions cuja
547
+ * sub-árvore inteira é sync não pagam o overhead de async functions.
548
+ *
549
+ * @param directives — diretivas normalizadas da action.
550
+ * @param analysis — slot analysis dos directive hooks (filled/async).
551
+ * @param typeField — campo de dispatch (default: "type").
552
+ * @param asyncHandlerSet — conjunto de tipos de handler marcados como async
553
+ * (via flag `async: true` ou auto-detect `isAsyncFunction`). Se omitido,
554
+ * nenhum handler é considerado async (compat backward).
555
+ * @param actionUsesAsyncOverride — se fornecido, substitui o cálculo local
556
+ * `directives.some(asyncHandlerSet.has)`. Usado pelo engine pra passar a
557
+ * info **transitiva** computada pelo mini-graph (ADR-026): action que
558
+ * invoca outra action async via `{ type: "action", id: X }` é async
559
+ * transitivamente, mas `asyncHandlerSet.has("action")` retorna false
560
+ * (handler `action` em si não é async). Sem essa override, JIT compilaria
561
+ * wrapper sync pra action async transitiva — bug.
562
+ *
563
+ * Hooks async (`hasAnyAsync`) sempre forçam wrapper async (independente
564
+ * do override) — wrapper sync não pode fazer await em hook.
565
+ */
566
+ declare function buildActionExecutor(directives: readonly Directive[], analysis: SlotAnalysis, typeField: string, asyncHandlerSet?: ReadonlySet<string>, actionUsesAsyncOverride?: boolean, actionIsInteractive?: boolean, interactiveHandlerSet?: ReadonlySet<string>): GeneratedActionExecutor;
308
567
 
309
- export { type ActionDefinition, type ActionHooks, type ActionInterceptResult, type DirectiveExecutorFn, type DirectiveHandler, type DirectiveHandlerMap, type DirectiveHooks, type DirectivePermission, type DirectivePermissionConfig, type DirectivePermissionEntry, type DirectivePermissionStatus, type DirectiveRunnerFn, type EngineEventMap, type EngineEventName, type GeneratedActionExecutor, type GeneratedDirectiveExecutor, type HandlerAnalysis, type HandlerDefinition, type HandlerInput, type HandlerInputMap, type IActionEngine, type IActionEngineConfig, type Listener, RESERVED_TYPES, type RegisterError, type RegisterEvent, type RegisterResult, SimpleEmitter, type UnregisterEvent, type ValidationResult, buildActionExecutor, buildDirectiveExecutor, createActionEngine, createDirectiveInterpreter, normalizeDirectives };
568
+ export { type ActionDefinition, type ActionHooks, type ActionInterceptResult, type AsyncInteractiveSession, type DirectiveExecutorFn, type DirectiveHandler, type DirectiveHandlerMap, type DirectiveHooks, type DirectivePermission, type DirectivePermissionConfig, type DirectivePermissionEntry, type DirectivePermissionStatus, type DirectiveRunnerFn, type EngineEventMap, type EngineEventName, type GeneratedActionExecutor, type GeneratedDirectiveExecutor, type HandlerAnalysis, type HandlerDefinition, type HandlerInput, type HandlerInputMap, type IActionEngine, type IActionEngineConfig, type InteractiveApplyResult, type InteractiveConfig, type InteractiveSession, type Listener, type PauseEvent, RESERVED_TYPES, type RegisterError, type RegisterEvent, type RegisterResult, type Responder, SimpleEmitter, type UnregisterEvent, type ValidationResult, buildActionExecutor, buildDirectiveExecutor, createActionEngine, createDirectiveInterpreter, drainAsync, drainSync, normalizeDirectives, replayResponder };