@statedelta-actions/actions 0.3.0 → 0.6.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/CHANGELOG.md ADDED
@@ -0,0 +1,96 @@
1
+ # @statedelta-actions/actions
2
+
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a3a818b: `HandlerDefinition` is now a discriminated union by execution mode
8
+
9
+ `HandlerDefinition<TCtx>` is `SyncHandlerDefinition<TCtx> | AsyncHandlerDefinition<TCtx> | InteractiveHandlerDefinition<TCtx>` — the `async` / `interactive` flags discriminate, and `execute` refines its return type per mode (`ApplyResult` for sync, `Promise<ApplyResult>` for async, `Generator<unknown, ApplyResult, unknown> | AsyncGenerator<unknown, ApplyResult, unknown>` for interactive). Consumers that call `handler.execute(...)` directly (tests, wrappers) — typing the handler as a subtype, or narrowing the union via the flags — no longer get the wide `ApplyResult | Promise<ApplyResult>` union for a handler they know is sync.
10
+
11
+ New exports: `HandlerBaseDefinition`, `SyncHandlerDefinition`, `AsyncHandlerDefinition`, `InteractiveHandlerDefinition`, `SyncHandlerExecute`, `AsyncHandlerExecute`, `InteractiveHandlerExecute`.
12
+
13
+ **Breaking (minor, pre-1.0):**
14
+ - A V2 handler that is `async` or `interactive` must now declare the `async: true` / `interactive: true` flag to satisfy the type. `{ async execute() {} }` without `async: true` no longer matches `HandlerDefinition` (it matches no subtype: sync requires a sync return, async/interactive require the flag). Runtime auto-detection via `isAsyncFunction` / `isGeneratorFunction` still works as a safety net — the type just forces the declaration to be explicit for V2.
15
+ - `HandlerDefinition<TCtx>["execute"]` is wider than before (it now includes the `Generator | AsyncGenerator` arm) — code that assigned `handler.execute` to `(...) => ApplyResult | Promise<ApplyResult>` needs to narrow or use a subtype. (This also fixes a latent bug: interactive handlers' `execute` previously violated the declared type silently.)
16
+ - V1 handlers (plain functions) and `HandlerInput<TCtx>` are unchanged.
17
+
18
+ ### Patch Changes
19
+
20
+ - 26c579a: Polish published package metadata and READMEs
21
+ - `@statedelta-actions/core` now ships a README (was missing on npm).
22
+ - `@statedelta-actions/analyzer` README: fixed pervasive missing accents.
23
+ - `graph` and `events` READMEs: titles are now the package name (`# @statedelta-actions/graph` / `# @statedelta-actions/events`) instead of a conceptual name.
24
+ - All packages: added `keywords`, `homepage`, `bugs`; package.json keys reordered to a canonical layout.
25
+ - All packages: `CHANGELOG.md` added to `files` so it ships to npm; a `LICENSE` (MIT) file is included in the repo root and in each package.
26
+
27
+ - Updated dependencies [26c579a]
28
+ - @statedelta-actions/core@0.5.1
29
+
30
+ ## 0.5.0
31
+
32
+ ### Minor Changes
33
+
34
+ - f8c2b4a: Fail-fast register errors, controlled `action-not-found`, atomic registration, and events async/interactive modes
35
+ - **actions** — A missing handler for a directive type now throws at register
36
+ time (was a collected error) — fail-fast for typos and forgotten handlers.
37
+ Invoking an unknown action id returns `aborted: true, abortedBy:
38
+ "action-not-found"` instead of a soft failure. `isActionAsync` is exposed on
39
+ the engine ref handed to handlers (symmetric with `isActionInteractive`).
40
+ - **rules** — `register()` is atomic: it builds a staging set, delegates to the
41
+ ActionEngine, then indexes locally only on success. Structural errors from the
42
+ ActionEngine propagate as a throw with no local state mutated.
43
+ - **events** — `register()`/`defineEvents()` are atomic the same way.
44
+ Async detection is now per-listener transitive via the ActionEngine mini-graph,
45
+ so hybrid engines keep `processEvents`/`processEventsAsync` viable for
46
+ listeners whose subtree is fully sync. New `processEventsInteractive()` returns
47
+ a drainable iterator (sync or async generator) — pauses from `type:"pause"` or
48
+ an interactive handler flow via `yield*` up to the consumer; halt scoping is
49
+ preserved (a pause stops the listeners of that event, following events
50
+ continue). New `isInteractive` getter.
51
+ - **core** — `ActionEngineRef` gains an optional `isActionAsync?` accessor.
52
+ - **sdk** — re-exports the new event types/surface; barrel deps moved to
53
+ `dependencies` (`workspace:^`).
54
+
55
+ ### Patch Changes
56
+
57
+ - Updated dependencies [f8c2b4a]
58
+ - @statedelta-actions/core@0.5.0
59
+
60
+ ## 0.3.0
61
+
62
+ ### Minor Changes
63
+
64
+ - Split types em definitions + engine em cada package; novo subpath sdk/definitions
65
+ - core: RegisterWarning movido pra core/types/engine.ts (tipo shared entre packages)
66
+ - actions: types.ts → types/definitions.ts + types/engine.ts
67
+ - rules: types.ts → types/definitions.ts + types/engine.ts
68
+ - events: types.ts → types/definitions.ts + types/engine.ts
69
+ - sdk: novo subpath ./definitions com re-export de definition types puros (zero runtime)
70
+
71
+ ### Patch Changes
72
+
73
+ - Updated dependencies []:
74
+ - @statedelta-actions/core@0.3.0
75
+
76
+ ## 0.2.0
77
+
78
+ ### Minor Changes
79
+
80
+ - Sync all packages to 0.2.0 (match SDK version)
81
+
82
+ ### Patch Changes
83
+
84
+ - Updated dependencies []:
85
+ - @statedelta-actions/core@0.2.0
86
+
87
+ ## 0.3.0
88
+
89
+ ### Minor Changes
90
+
91
+ - Sync all packages to 0.2.0 (match SDK version)
92
+
93
+ ### Patch Changes
94
+
95
+ - Updated dependencies []:
96
+ - @statedelta-actions/core@0.3.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anderson D. Rosa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -16,6 +16,8 @@ Ações são **declarativas**. Uma action é um ID mais uma lista ordenada de di
16
16
 
17
17
  **Sem sugar no runtime.** Diretivas devem ser passadas em **forma canônica** — toda diretiva tem campo `type`. Sugar forms (shorthands de autoria) são concern do compilador JSON DSL, que normaliza antes de entregar pro engine. O engine não interpreta nem converte sugar.
18
18
 
19
+ **Modo interactive opcional.** Actions podem **pausar entre diretivas** e aguardar input externo via generators (sync ou async). Implementado de forma ortogonal ao modo async — a granularidade é per-action transitiva via mini-graph interno. Ver [Modo Interactive](#modo-interactive) abaixo.
20
+
19
21
  ## Instalação
20
22
 
21
23
  ```bash
@@ -128,6 +130,47 @@ const handlers = {
128
130
  | `validate` | Register | Validação estrutural (rejeita diretivas malformadas) |
129
131
  | `execute` | Runtime | Processa a diretiva e retorna resultado |
130
132
  | `analyze` | — | Consumido pelo `ActionAnalyzer` externo, não pelo engine |
133
+ | `async` (flag) | Construct | Marca handler como assíncrono (opt-in explícito) |
134
+
135
+ ### Handlers Async
136
+
137
+ Handlers podem retornar `Promise<ApplyResult>`. O engine detecta async de duas formas:
138
+
139
+ ```typescript
140
+ const handlers = {
141
+ // 1. Auto-detect via `async function` — pega quando execute é declarado async.
142
+ fetchUser: {
143
+ async execute(directive, frame) {
144
+ const user = await api.get(`/users/${directive.id}`);
145
+ return { ok: true, data: user };
146
+ },
147
+ },
148
+
149
+ // 2. Flag explícita — para wrappers que retornam Promise sem ser `async function`.
150
+ delegateAsync: {
151
+ async: true,
152
+ execute: (d, frame, engine) => engine.invokeAsync("inner", undefined, frame),
153
+ },
154
+ };
155
+ ```
156
+
157
+ **Engine híbrido.** O engine pode misturar handlers sync e async no mesmo registry. Se qualquer handler for async (ou qualquer hook for async), o engine inteiro vira async — `invoke()` lança e você passa a usar `invokeAsync()`. Mas o **JIT per-action decide sync/async por action**: actions que só usam handlers sync compilam wrapper sync, sem `await`. FPS/games com 100% handlers sync pagam zero overhead de async; ETL/business com handlers async awaita só onde precisa.
158
+
159
+ ```typescript
160
+ const engine = createActionEngine({
161
+ handlers: {
162
+ update: { execute: (d, f) => { /* sync */ return { ok: true }; } },
163
+ fetchDB: { async execute(d, f) { return { ok: true, data: await db.query(...) }; } },
164
+ },
165
+ });
166
+
167
+ engine.isAsync; // true (fetchDB é async)
168
+ engine.invoke("anything"); // throws — use invokeAsync
169
+
170
+ // JIT compila:
171
+ // action que usa só `update` → wrapper sync, zero await
172
+ // action que usa `fetchDB` → wrapper async, await no handler
173
+ ```
131
174
 
132
175
  ## Ações
133
176
 
@@ -156,11 +199,13 @@ Três categorias. **Toda diretiva tem campo `type`** (forma canônica). O engine
156
199
  { type: "let", name: "total", value: "$", resolve: (ctx, scope) => ({ value: scope.subtotal * 1.1 }) }
157
200
  ```
158
201
 
159
- **Control** — saída antecipada:
202
+ **Control** — saída antecipada e ramificação:
160
203
 
161
204
  ```typescript
162
205
  { type: "return", value: "done" } // success: true, data: "done"
163
206
  { type: "throw", message: "saldo insuficiente" } // success: false
207
+ { type: "if", cond: (ctx, scope) => scope.hp > 0,
208
+ then: [...], else: [...] } // ramificação inline
164
209
  ```
165
210
 
166
211
  **Handler** — dispatch pro handler registrado:
@@ -172,7 +217,21 @@ Três categorias. **Toda diretiva tem campo `type`** (forma canônica). O engine
172
217
 
173
218
  O interpreter e JIT operam sobre formato uniforme — um único dispatch via `type` field, sem branching de categorias.
174
219
 
175
- Os nomes `const`, `let`, `return`, `throw` são **tipos reservados** — o engine rejeita handlers com esses nomes.
220
+ Os nomes `const`, `let`, `return`, `throw`, `if`, `pause` são **tipos reservados** — o engine rejeita handlers com esses nomes.
221
+
222
+ ### `if` (then/else)
223
+
224
+ `cond` aceita função `(ctx, scope) => boolean` ou boolean literal. Branches executam **no mesmo scope da action** — `const`/`let` em branch ficam visíveis depois. `return`/`throw`/`halt` em branch saem da action inteira (semântica esperada). Aninhamento livre.
225
+
226
+ ```typescript
227
+ { type: "if",
228
+ cond: (_ctx, scope) => scope.hp > 0,
229
+ then: [{ type: "state", target: "status", value: "alive" }],
230
+ else: [{ type: "throw", message: "dead" }],
231
+ }
232
+ ```
233
+
234
+ `cond` lança → erro coletado em `errors[]`, nenhum branch executa, prossegue. JIT emite `if/else` JS nativo unrolled (zero call overhead).
176
235
 
177
236
  Diretivas suportam:
178
237
  - **`as`** — captura `result.data` no scope: `scope["prev"] = result.data`
@@ -240,7 +299,7 @@ engine.context(ctx, () => {
240
299
  engine.invoke("combat");
241
300
  });
242
301
 
243
- // Async (obrigatório se algum hook for async)
302
+ // Async (obrigatório se algum hook ou handler for async)
244
303
  const result = await engine.invokeAsync("heal", undefined, ctx);
245
304
  ```
246
305
 
@@ -353,6 +412,8 @@ createActionEngine({ handlers, mode: "auto" }); // Interpreta primeiro, pr
353
412
  | `jit` | Mais lento (compila) | `new Function` compilado | Produção, ações estáveis |
354
413
  | `auto` | Rápido | Promove per-action após threshold | Uso geral (padrão) |
355
414
 
415
+ **Decisão sync/async é per-action.** No JIT per-action, cada action é compilada sync ou async independentemente, baseado nos handlers que ela usa. Em um engine híbrido, actions 100% sync ficam com wrapper sync e zero `await`; actions com pelo menos um handler async ficam com wrapper `async` e `await` em todos os handlers da action. Isso garante que uso fully-sync (FPS, game loops) não pague o custo de async functions só porque outro handler do mesmo engine é async.
416
+
356
417
  Threshold de auto-promote (padrão: 8):
357
418
 
358
419
  ```typescript
@@ -368,7 +429,7 @@ engine.compile();
368
429
  Informações de compilação:
369
430
 
370
431
  ```typescript
371
- engine.isAsync; // true se hooks async detectados
432
+ engine.isAsync; // true se algum hook OU handler é async
372
433
  engine.compilationMode; // "interpret" | "jit" (modo atual, não o requestado)
373
434
  ```
374
435
 
@@ -394,7 +455,7 @@ createActionEngine({
394
455
  });
395
456
  ```
396
457
 
397
- Hooks podem ser sync ou async. Hooks async tornam o engine async (`engine.isAsync === true`), exigindo `invokeAsync()`.
458
+ Hooks podem ser sync ou async. Hooks async tornam o engine async (`engine.isAsync === true`), exigindo `invokeAsync()`. Handlers async (via flag `async: true` ou `async function`) também tornam o engine async — ver [Handlers Async](#handlers-async).
398
459
 
399
460
  **Custo zero quando ausente.** A compilação JIT não emite código de hook para hooks não registrados.
400
461
 
@@ -569,6 +630,22 @@ import type {
569
630
  ActionInterceptResult,
570
631
  } from "@statedelta-actions/actions";
571
632
 
633
+ // Interactive
634
+ import type {
635
+ InteractiveConfig,
636
+ PauseEvent,
637
+ InteractiveSession,
638
+ AsyncInteractiveSession,
639
+ InteractiveApplyResult,
640
+ Responder,
641
+ } from "@statedelta-actions/actions";
642
+
643
+ import {
644
+ drainSync,
645
+ drainAsync,
646
+ replayResponder,
647
+ } from "@statedelta-actions/actions";
648
+
572
649
  // Register Pipeline
573
650
  import { RESERVED_TYPES } from "@statedelta-actions/actions";
574
651
 
@@ -588,6 +665,184 @@ import { buildActionExecutor } from "@statedelta-actions/actions";
588
665
  import type { GeneratedActionExecutor } from "@statedelta-actions/actions";
589
666
  ```
590
667
 
668
+ ## Modo Interactive
669
+
670
+ Pausa execução de uma action e aguarda input externo do customer. Implementado via **generators** (sync ou async), ortogonal ao modo async.
671
+
672
+ **Use cases:**
673
+ - UX interativa em produção (wizards, prompts, confirmações)
674
+ - Debug entre diretivas (futuro — modo separado)
675
+
676
+ **Mecânica:**
677
+ - **Handler interactive** declarado com `interactive: true` (espelha `async: true`) — handler é generator function
678
+ - **Diretiva reservada `type: "pause"`** — engine emite yield direto pra confirmação/breakpoint declarativo
679
+ - **Mini-graph interno** propaga `_interactiveActions` transitivamente (ADR-026)
680
+ - **API**: `engine.invokeInteractive(id, params, ctx)` retorna `Iterator | AsyncIterator`
681
+
682
+ ### Habilitação
683
+
684
+ ```typescript
685
+ const engine = createActionEngine({
686
+ handlers,
687
+ interactive: {}, // habilita modo interactive
688
+ });
689
+ ```
690
+
691
+ Sem `interactive` configurado:
692
+ - Handler com `interactive: true` → erro no constructor (fail-fast)
693
+ - Diretiva `type: "pause"` → erro no register
694
+
695
+ ### Handler interactive
696
+
697
+ Handlers são **primitivas Lego** — genéricas, sem semântica de domínio (ADR-028). Vocabulário canonical pequeno (`state`, `emit`, `action`, `log`, `halt`) + handler interactive primitivo proposto: **`input`** (yield + schema + retry).
698
+
699
+ > ⚠️ **Não use nomes domain-specific** (`askUser`, `confirmDelete`, `validateEmail`). Cria explosão combinatória, polui vocabulário canonical e viola o princípio Lego. Customer compõe domínio via **payload** das diretivas, não criando handlers.
700
+
701
+ ```typescript
702
+ // Pattern recomendado: primitiva `input` genérica com schema + retry interno
703
+ const handlers = {
704
+ input: {
705
+ interactive: true,
706
+ *execute(directive) {
707
+ let lastError: string | null = null;
708
+ let attempt = 1;
709
+ const max = directive.maxAttempts ?? Infinity;
710
+ while (attempt <= max) {
711
+ const answer = yield {
712
+ kind: "input",
713
+ payload: directive.payload,
714
+ schema: directive.schema,
715
+ attempt,
716
+ lastError,
717
+ };
718
+ if (!directive.schema) return { ok: true, data: answer };
719
+ const r = directive.schema.parse(answer);
720
+ if (r.ok) return { ok: true, data: r.data };
721
+ lastError = r.error;
722
+ attempt++;
723
+ }
724
+ return { ok: false, error: `validation failed after ${max} attempts` };
725
+ },
726
+ },
727
+ };
728
+ ```
729
+
730
+ Customer compõe actions com **payload domain-specific** sobre a primitiva:
731
+
732
+ ```typescript
733
+ { id: "register", directives: [
734
+ { type: "input", payload: { kind: "text", label: "Nome" }, as: "name", schema: nameSchema },
735
+ { type: "input", payload: { kind: "number", label: "Idade" }, as: "age", schema: ageSchema },
736
+ { type: "input", payload: { kind: "confirm", message: "Confirma?" }, as: "ok" },
737
+ { type: "state", target: "users", op: "push", value: { name: ..., age: ... } },
738
+ ]}
739
+ ```
740
+
741
+ Consumer interpreta `payload.kind` — renderiza UI/CLI/voice. Framework não opina.
742
+
743
+ **Schema agnóstico** — interface mínima `{ parse(raw): { ok: true, data } | { ok: false, error } }`. Customer adapta zod/valibot/json-schema/custom em 1 linha.
744
+
745
+ > **`input` (intra-tick) ≠ `request` (cross-tick).** `input` pausa **dentro** de uma invocação (yield/next imediato). `request` (RealmSystem futuro, fora deste package) anota dependência declarativa, lock cross-tick. São conceitos distintos com nomes propositalmente diferentes.
746
+
747
+ Auto-detect via `isGeneratorFunction(execute)` cobre `function*` e `async function*`. Use a flag `interactive: true` pra wrappers que retornam iterator sem ser generator function.
748
+
749
+ ### Diretiva `type: "pause"` (engine-level)
750
+
751
+ ```typescript
752
+ { type: "pause", message: "Confirmação destrutiva?", as: "ack" }
753
+ ```
754
+
755
+ Engine yield direto:
756
+ ```typescript
757
+ session.next(); // → { source: "pause", payload: { message: "..." }, ... }
758
+ session.next("ok"); // → continua, captura "ok" em scope.ack
759
+ session.next(false); // → aborta (abortedBy: "pause")
760
+ session.next("cancel"); // → aborta
761
+ session.next("abort"); // → aborta
762
+ ```
763
+
764
+ ### Drenagem do iterator
765
+
766
+ ```typescript
767
+ // Customer dirigindo manualmente
768
+ const session = engine.invokeInteractive("wizard", undefined, ctx);
769
+ const r1 = session.next(); // PauseEvent ou payload custom
770
+ const r2 = session.next("Anderson"); // entrega resposta
771
+ // quando r.done === true, r.value é o DirectiveResult final
772
+ ```
773
+
774
+ Helpers opcionais pra "play mode":
775
+
776
+ ```typescript
777
+ import { drainSync, drainAsync, replayResponder } from "@statedelta-actions/actions";
778
+
779
+ // Drena com responder programático
780
+ const result = drainSync(session, (event) => {
781
+ // event pode ser PauseEvent (type:"pause") ou payload custom (handler)
782
+ if ("prompt" in event) return responses[event.prompt];
783
+ if ((event as PauseEvent).source === "pause") return "ok";
784
+ return undefined;
785
+ });
786
+
787
+ // Replay determinístico (testes)
788
+ const result = drainSync(session, replayResponder(["Anderson", 42, "yes"]));
789
+ ```
790
+
791
+ ### Sub-actions interativas (yield* propaga pausas)
792
+
793
+ Quando action root invoca child interactive, pausas do child fluem pro consumer no nível raiz via `yield*`. Handler `action` (consumer-defined) decide:
794
+
795
+ ```typescript
796
+ const handlers = {
797
+ action: {
798
+ execute: (d, f, e) => {
799
+ if (e.isActionInteractive(d.id)) {
800
+ // Target é interactive → propaga via iterator
801
+ return { ok: true, iterator: e.invokeInteractive(d.id, d.params, f) };
802
+ }
803
+ // Target sync → invoke regular
804
+ const r = e.invoke(d.id, d.params, f);
805
+ return { ok: r.success, data: r.data };
806
+ },
807
+ },
808
+ };
809
+ ```
810
+
811
+ Engine consulta mini-graph e marca actions transitivamente interativas. JIT detecta `_result.iterator` em runtime e emite `yield*` automaticamente.
812
+
813
+ ### Matriz async × interactive
814
+
815
+ | Engine async? | Action interactive? | Compilação |
816
+ |---------------|---------------------|------------|
817
+ | não | não | `function ()` |
818
+ | não | sim | `function* ()` |
819
+ | sim | não | `async function ()` |
820
+ | sim | sim | `async function* ()` |
821
+
822
+ Granularidade per-action via mini-graph (ADR-021/025/026). FPS 100% sync com 1 action interactive isolada não paga overhead de generator nas outras.
823
+
824
+ ### `invoke()` per-action transitivo
825
+
826
+ `invoke()` lança per-action (não global):
827
+
828
+ ```typescript
829
+ engine.invoke("syncAction"); // OK — sub-árvore inteira sync
830
+ engine.invoke("asyncAction"); // throw "use invokeAsync"
831
+ engine.invoke("interactiveAction"); // throw "use invokeInteractive"
832
+ ```
833
+
834
+ Engine híbrido (handler async + actions sync isoladas) permite `invoke()` regular nas sync. ADR-026.
835
+
836
+ ### Action hooks fora do generator
837
+
838
+ `beforeAction` / `afterAction` (ADR-019) executam **antes** e **depois** do generator. Skip via `beforeAction` retorna `memoResult` imediatamente. `afterAction` substitui o resultado final.
839
+
840
+ ### Catch atômico
841
+
842
+ Diretivas dentro de `catch` executam **atomicamente** (ADR-023) via `engine.runDirectives` — sem yield. Caminho de erro não pausa. Diretivas `type: "pause"` ou handler interactive dentro de catch geram warning em register-time.
843
+
844
+ ---
845
+
591
846
  ## Analyzer
592
847
 
593
848
  Funcionalidades de análise estática foram extraídas para o pacote `@statedelta-actions/analyzer`: