@softize/opus 20.0.0 → 21.0.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 +141 -0
- package/bin/lib/gen-manifest.mjs +1 -0
- package/bin/lib/gen-runner.mjs +3 -0
- package/bin/lib/introspect.mjs +5 -4
- package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +8 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +14 -1
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
- package/docs/adr/0026-reactions-apply-declared-delivery-policy.md +123 -0
- package/docs/adr/0027-reactions-publish-declared-events-with-bounded-chains.md +96 -0
- package/docs/adr/0028-schedules-run-actions-as-the-system-by-action-opt-in.md +127 -0
- package/docs/protocol.md +60 -24
- package/package.json +1 -1
- package/src/core/audit.ts +2 -1
- package/src/core/index.ts +1 -1
- package/src/core/reactions.ts +6 -2
- package/src/core/runtime.ts +598 -70
- package/src/core/schedules.ts +3 -2
- package/src/core/types.ts +52 -7
- package/src/testing/index.ts +25 -14
- package/src/ui/components/patterns/content-header.tsx +94 -15
- package/src/ui/components/patterns/list.tsx +2 -14
- package/src/ui/components/patterns/surface-header.tsx +58 -25
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +1 -1
- package/src/ui/components/primitives/drawer.tsx +1 -1
- package/src/ui/docs/content/action-list.md +7 -6
- package/src/ui/docs/content/actions.md +2 -1
- package/src/ui/docs/content/audit.md +10 -0
- package/src/ui/docs/content/auth.md +5 -1
- package/src/ui/docs/content/content.md +88 -20
- package/src/ui/docs/content/events.md +109 -11
- package/src/ui/docs/content/icon-tile.md +11 -11
- package/src/ui/docs/content/runtime.md +11 -2
- package/src/ui/docs/content/scheduler.md +53 -12
- package/src/ui/docs/content/testing.md +14 -2
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# ADR 0028 — Schedules executam actions como o sistema, por opt-in da action
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita (21.0.0).
|
|
4
|
+
- **Data:** 2026-09-26.
|
|
5
|
+
|
|
6
|
+
## Contexto
|
|
7
|
+
|
|
8
|
+
Um schedule executa a action com `user: null` e um `can` que nega tudo. Para o runtime, isso é
|
|
9
|
+
indistinguível de um chamador anônimo: a action alvo só roda se declarar `public: true`. A
|
|
10
|
+
documentação ensinava a combinar `public: true` com `internal: true`, para que os servidores do Opus
|
|
11
|
+
não a expusessem por HTTP. O contorno tem três defeitos:
|
|
12
|
+
|
|
13
|
+
- `public: true` declara que a action não exige identidade, o contrário do que se quer dizer;
|
|
14
|
+
- `internal` só protege nos servidores do Opus; outro transporte que chame `runtime.execute` expõe
|
|
15
|
+
uma action pública sem autenticação;
|
|
16
|
+
- o `authorize` da action não distingue o sistema de um anônimo, porque os dois chegam com
|
|
17
|
+
`user: null`.
|
|
18
|
+
|
|
19
|
+
O contrato já tem, sem efeito até aqui, o campo `automatable`, documentado no protocolo como
|
|
20
|
+
“sistema/cron/workflow pode disparar”. A provenance `schedule` e o `actor.type: 'system'` do audit
|
|
21
|
+
também já existem.
|
|
22
|
+
|
|
23
|
+
Restrições dadas pelo dono, inegociáveis: nada que hoje exige usuário passa a ser executável por
|
|
24
|
+
HTTP sem usuário; o acesso do sistema é opt-in por action e negado por padrão; `authorize` e
|
|
25
|
+
`requires` enxergam que o ator é o sistema; o audit registra o ator.
|
|
26
|
+
|
|
27
|
+
## Decisão
|
|
28
|
+
|
|
29
|
+
1. **O opt-in é `automatable: true` no contrato da action.** O campo existente passa a ter efeito:
|
|
30
|
+
uma action automatizável aceita execução pelo sistema sem usuário. Sem ele, a execução pelo
|
|
31
|
+
sistema de uma action não pública é recusada com `auth.system_forbidden` (categoria
|
|
32
|
+
`authorization`). O opt-in fica no contrato, e não no schedule, para que quem é dono da action
|
|
33
|
+
decida e para que o manifest, que passa a projetar `automatable`, mostre a decisão a quem revisa.
|
|
34
|
+
2. **Só o runtime cria a identidade de sistema.** O disparo de um schedule executa a action por um
|
|
35
|
+
caminho interno do runtime, marcado como execução do sistema; `runtime.execute` não aceita essa
|
|
36
|
+
marca. Um `ContextBase` com `provenance: { kind: 'schedule' }` vindo de fora continua sendo, sem
|
|
37
|
+
usuário, um chamador anônimo. Um job em background preserva a identidade por uma marca
|
|
38
|
+
explícita no envelope, `ctx.system: true`, que o runtime grava ao enfileirar somente quando a
|
|
39
|
+
execução que enfileira é do sistema (`isSystemActor`). O worker exige essa marca e `userId: null`;
|
|
40
|
+
a provenance original não conta, porque vem do chamador de `runtime.execute` e pode dizer
|
|
41
|
+
`schedule` sem que um schedule tenha disparado. Pelo mesmo motivo, uma tool chamada por
|
|
42
|
+
`ctx.ai` dentro de uma action agendada não herda a identidade, nem no job que ela enfileira.
|
|
43
|
+
3. **A identidade reaproveita o contexto existente.** Na execução pelo sistema, `ctx.user` é `null`
|
|
44
|
+
(não há pessoa), `ctx.can` nega toda permissão e `ctx.provenance` é
|
|
45
|
+
`{ kind: 'schedule', scheduleName }` — ou `background` com essa provenance original. O helper
|
|
46
|
+
`isSystemActor(ctx)`, exportado pelo core, responde se o ator é o sistema, para que um
|
|
47
|
+
`authorize` possa aceitá-lo de forma explícita: `(ctx) => isSystemActor(ctx) ||
|
|
48
|
+
ctx.can('caixa:fechar')`. O helper não lê a provenance, que qualquer código servidor pode
|
|
49
|
+
preencher: ele consulta um registro interno dos contextos que o próprio runtime criou para o
|
|
50
|
+
sistema. `requires` continua declarativo: o sistema não detém permissões, e o manifest mostra
|
|
51
|
+
`automatable` ao lado de `permission`.
|
|
52
|
+
4. **O audit registra o ator como sistema.** O registro de uma execução pelo sistema traz
|
|
53
|
+
`actor: { id: null, type: 'system' }` e a provenance com o nome do schedule, como já acontecia
|
|
54
|
+
com actions públicas agendadas.
|
|
55
|
+
5. **O `start()` recusa schedules que não vão rodar.** Um schedule ativo cuja action não é `public`
|
|
56
|
+
nem `automatable` interrompe o boot com `runtime.schedule_action_not_automatable`, antes de
|
|
57
|
+
inicializar os adapters, como `runtime.schedule_action_not_found`. A mensagem lista cada schedule
|
|
58
|
+
e a action, e indica as duas saídas: `automatable: true` no contrato ou `enabled: false` no
|
|
59
|
+
schedule. Um aviso deixaria o processo subir com um agendamento que falha em todo disparo; o
|
|
60
|
+
custo de quebrar o boot de quem tem essa configuração foi aceito junto com a versão major
|
|
61
|
+
(21.0.0), que já reúne as outras mudanças incompatíveis desta decisão e da ADR 0026.
|
|
62
|
+
|
|
63
|
+
`public: true` continua funcionando para actions agendadas e mantém o comportamento de hoje; a
|
|
64
|
+
documentação passa a recomendar `automatable: true`.
|
|
65
|
+
|
|
66
|
+
## Consequências
|
|
67
|
+
|
|
68
|
+
- Uma action pode ser agendada sem ficar aberta a anônimos. `internal: true` deixa de ser
|
|
69
|
+
necessário para esse fim e volta a significar só “não expor por REST”.
|
|
70
|
+
- Um `authorize` que delega a `ctx.can` recusa o sistema até aceitá-lo com `isSystemActor(ctx)`.
|
|
71
|
+
São duas decisões explícitas — `automatable` e a regra no `authorize` — quando a action tem
|
|
72
|
+
autorização própria; sem `authorize`, `automatable` basta.
|
|
73
|
+
- Uma chamada de IA feita pelo handler de uma action agendada (`ctx.ai.run`) não herda a identidade
|
|
74
|
+
de sistema: as tools passam por `runtime.execute`, como qualquer chamador.
|
|
75
|
+
- `actions` que já declaravam `automatable: true` passam a aceitar execução por schedule. Como só um
|
|
76
|
+
schedule registrado no próprio runtime cria a identidade, nenhuma rota HTTP ganha acesso.
|
|
77
|
+
- O harness `runAction` ganha a opção `schedule`, que executa a action como o sistema com a mesma
|
|
78
|
+
regra do runtime.
|
|
79
|
+
- No worker, a identidade vem do envelope do job, como o `userId` já vinha. Escrever diretamente na
|
|
80
|
+
fila fica **fora do modelo de ameaça** desta decisão: quem tem esse acesso já forja o `userId` de
|
|
81
|
+
qualquer pessoa, e a marca `system` não amplia o que ele alcança. A fronteira de confiança da fila
|
|
82
|
+
não muda; protegê-la é papel da infraestrutura (credenciais do Redis, rede).
|
|
83
|
+
- Um schedule sem opt-in, que antes subia e falhava em cada disparo com `auth.unauthenticated`,
|
|
84
|
+
agora impede o boot. `auth.system_forbidden` continua existindo para o caso que o boot não
|
|
85
|
+
detecta: um job do sistema cuja action perdeu `automatable` entre o enfileiramento e o worker.
|
|
86
|
+
|
|
87
|
+
## Alternativas descartadas
|
|
88
|
+
|
|
89
|
+
### Um usuário sintético de sistema em `ctx.user`
|
|
90
|
+
|
|
91
|
+
Deixaria `authorize` existente funcionar sem mudança. Foi descartada porque um `authorize` que
|
|
92
|
+
testa apenas `ctx.user !== null` passaria a aceitar o sistema sem que ninguém decidisse isso, e
|
|
93
|
+
porque um id sintético se misturaria aos ids de pessoas no audit.
|
|
94
|
+
|
|
95
|
+
### Opt-in no schedule
|
|
96
|
+
|
|
97
|
+
Colocaria a decisão ao lado do agendamento. Foi descartada porque o schedule concederia acesso a uma
|
|
98
|
+
action que ele não possui, e porque o contrato da action — e o manifest — deixariam de mostrar quem
|
|
99
|
+
pode executá-la.
|
|
100
|
+
|
|
101
|
+
### Um campo novo `ctx.actor` ou um tipo de identidade próprio
|
|
102
|
+
|
|
103
|
+
Tornaria a identidade mais evidente. Foi adiado: provenance e `user: null` já descrevem o ator sem
|
|
104
|
+
ambiguidade quando só o runtime cria a execução pelo sistema, e um campo novo exigiria migração em
|
|
105
|
+
todos os consumidores do contexto.
|
|
106
|
+
|
|
107
|
+
### Um `can` de sistema que concede permissões configuráveis
|
|
108
|
+
|
|
109
|
+
Resolveria `authorize` baseado em `ctx.can`. Foi descartada por ora porque criaria uma política de
|
|
110
|
+
permissões do sistema paralela à do app; a regra explícita no `authorize` é mais fácil de revisar.
|
|
111
|
+
|
|
112
|
+
### Só avisar no boot quando o schedule aponta para action não automatizável
|
|
113
|
+
|
|
114
|
+
Foi a proposta inicial: não quebraria projetos que sobem hoje. Foi descartada na decisão do dono,
|
|
115
|
+
porque o processo subiria com um schedule que falha em todo disparo e o aviso se perde no log; como
|
|
116
|
+
a versão já é major, a falha no boot fica coerente com `runtime.schedule_action_not_found`.
|
|
117
|
+
|
|
118
|
+
## Verificação
|
|
119
|
+
|
|
120
|
+
`tests/core/system-actor.test.ts` confere, pela fronteira do runtime: schedule com action
|
|
121
|
+
`automatable` executa sem `public`; `runtime.execute` sem usuário, mesmo com provenance `schedule`,
|
|
122
|
+
continua recusado; `isSystemActor` é verdadeiro só na execução pelo sistema, inclusive no job em
|
|
123
|
+
background criado por schedule, e falso para uma provenance `schedule` montada fora do runtime —
|
|
124
|
+
também no job que essa chamada enfileira e na tool em background chamada por `ctx.ai` dentro de uma
|
|
125
|
+
action agendada; a falha de boot antes da inicialização dos adapters, inclusive com schedule
|
|
126
|
+
desabilitado ignorado; a recusa `auth.system_forbidden` de um job do sistema para action sem
|
|
127
|
+
opt-in; e o harness `runAction` com a opção `schedule`.
|
package/docs/protocol.md
CHANGED
|
@@ -98,7 +98,7 @@ type ActionBase<In, Out> = {
|
|
|
98
98
|
|
|
99
99
|
// — Integração —
|
|
100
100
|
ai?: boolean | AIConfig // default true (opt-out via false)
|
|
101
|
-
automatable?: boolean //
|
|
101
|
+
automatable?: boolean // schedule pode executar como o sistema (§4, ADR 0028)
|
|
102
102
|
internal?: boolean // só service-to-service; não expõe REST
|
|
103
103
|
successStatus?: number // HTTP code de sucesso (default 200)
|
|
104
104
|
}
|
|
@@ -606,6 +606,20 @@ O Opus **não autentica** — não valida token, não emite sessão. O adapter (
|
|
|
606
606
|
|
|
607
607
|
Se `ctx.user` é `null` e `public !== true`, runtime rejeita com `{ code: 'auth.unauthenticated', category: 'authentication' }` **antes** dos loaders e de `authorize`. Assim um chamador anônimo não dispara consultas nem descobre, pelo erro de um loader, se um registro existe.
|
|
608
608
|
|
|
609
|
+
### Identidade de sistema
|
|
610
|
+
|
|
611
|
+
O disparo de um schedule executa a action **como o sistema**: `ctx.user` é `null`, `ctx.can` nega toda permissão e `ctx.provenance` é `{ kind: 'schedule', scheduleName }` (ou `background` com essa provenance original, no job que o schedule enfileirou). Só o runtime cria essa execução; `runtime.execute` não a aceita de fora, mesmo com uma provenance `schedule`. O job carrega a identidade pela marca `JobSpec.ctx.system`, gravada no enfileiramento só quando a execução é do sistema; o worker não a deduz da provenance.
|
|
612
|
+
|
|
613
|
+
O acesso do sistema é **opt-in por action** e negado por padrão. Sem usuário, o gate de autenticação aceita o sistema apenas quando a action declara `automatable: true`; nas demais, responde `{ code: 'auth.system_forbidden', category: 'authorization' }`. Uma action `public: true` continua aceitando qualquer chamador, inclusive o sistema.
|
|
614
|
+
|
|
615
|
+
`isSystemActor(ctx)` responde se o ator é o sistema. Ele consulta um registro interno dos contextos que o runtime criou, e não a provenance. Como o sistema não detém permissões, um `authorize` que delega a `ctx.can` o aceita de forma explícita, numa closure — a DSL em string não expressa isso, e `ctx.provenance` não serve como teste de sistema, porque o chamador de `runtime.execute` pode preenchê-la:
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
authorize: (ctx) => isSystemActor(ctx) || ctx.can('caixa:fechar')
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`requires` continua declarativo; o manifest projeta `automatable` ao lado de `permission`. O audit registra `actor: { id: null, type: 'system' }`. Ver ADR 0028.
|
|
622
|
+
|
|
609
623
|
---
|
|
610
624
|
|
|
611
625
|
## 5. Contrato de audit
|
|
@@ -719,6 +733,10 @@ registerAuditSink({
|
|
|
719
733
|
|
|
720
734
|
Múltiplos sinks são permitidos. Action sem `audit.sink` emite para todos os sinks registrados; com `audit.sink: 'compliance'` emite só pro sink nomeado.
|
|
721
735
|
|
|
736
|
+
### Registros de reaction
|
|
737
|
+
|
|
738
|
+
Reactions também auditam: um registro por entrega que chega ao handler, com `action` igual ao nome da reaction, `provenance.kind: 'reaction'`, `meta: { kind: 'reaction', attempts }`, sem `actionKind` e com `input: { event: { id, type, source } }` — sem o payload, que já está no audit da action emissora. Registros de reaction vão para todos os sinks. Ver §12.
|
|
739
|
+
|
|
722
740
|
### Falha de sink
|
|
723
741
|
|
|
724
742
|
Falha ao emitir audit **não** falha a action. Sink lançou → opus loga `{ code: 'audit.sink.failed', cause: <error> }` no logger configurado e continua. Persistência de audit é responsabilidade do sink (retry, fila, etc).
|
|
@@ -1517,10 +1535,16 @@ type DomainEvent<T = unknown> = {
|
|
|
1517
1535
|
tenant?: string | null
|
|
1518
1536
|
data: T // payload (livre, handler decide)
|
|
1519
1537
|
source: {
|
|
1520
|
-
action: string // 'deal.archive'
|
|
1521
|
-
actionId: string // bate com AuditRecord.id
|
|
1538
|
+
action: string // action que iniciou a cadeia ('deal.archive')
|
|
1539
|
+
actionId: string // bate com AuditRecord.id dessa action
|
|
1522
1540
|
correlation?: string // request id / trace
|
|
1523
1541
|
}
|
|
1542
|
+
cause?: { // só em evento publicado por reaction (§12)
|
|
1543
|
+
reaction: string // reaction que publicou
|
|
1544
|
+
eventId: string // evento que ela processava
|
|
1545
|
+
eventType: string
|
|
1546
|
+
depth: number // 1 = primeira reaction depois da action
|
|
1547
|
+
}
|
|
1524
1548
|
trace?: TraceContext // propagado para reactions/consumidores
|
|
1525
1549
|
meta?: Record<string, unknown>
|
|
1526
1550
|
}
|
|
@@ -1577,10 +1601,11 @@ type ReactionDef<Event = unknown> = {
|
|
|
1577
1601
|
// — Execução —
|
|
1578
1602
|
handler: (ctx: ReactionContext, event: DomainEvent<Event>) =>
|
|
1579
1603
|
Promise<void> | void
|
|
1604
|
+
emits?: string[] // eventos que o handler pode publicar
|
|
1580
1605
|
|
|
1581
1606
|
// — Resiliência —
|
|
1582
|
-
retry?: { attempts: number, backoff?: BackoffSpec }
|
|
1583
|
-
timeout?: number // ms
|
|
1607
|
+
retry?: { attempts: number, backoff?: BackoffSpec } // attempts = total de execuções
|
|
1608
|
+
timeout?: number // ms por tentativa; aborta ctx.signal
|
|
1584
1609
|
dedup?: (event: DomainEvent<Event>) => string // chave de idempotência
|
|
1585
1610
|
concurrency?: number // máx execuções paralelas (default 1)
|
|
1586
1611
|
|
|
@@ -1628,16 +1653,21 @@ type ReactionContext = {
|
|
|
1628
1653
|
user: User | null // ator que disparou o evento (pode ser system)
|
|
1629
1654
|
tenantId: string | null
|
|
1630
1655
|
db: unknown // mesmo data adapter
|
|
1631
|
-
emit: EmitFn //
|
|
1656
|
+
emit: EmitFn // publica só os eventos de `emits` (encadeamento limitado)
|
|
1632
1657
|
log: LoggerAdapter // logger pre-bound com contexto da reaction
|
|
1633
1658
|
storage: StorageAdapter | null // storage de arquivos (experimental)
|
|
1634
1659
|
provenance: Provenance // { kind: 'reaction', … }
|
|
1635
1660
|
trace?: TraceContext // span filho do evento recebido
|
|
1636
1661
|
meta: Record<string, unknown>
|
|
1662
|
+
signal?: AbortSignal // abortado quando a tentativa excede `timeout`
|
|
1637
1663
|
}
|
|
1638
1664
|
```
|
|
1639
1665
|
|
|
1640
|
-
|
|
1666
|
+
### Encadeamento
|
|
1667
|
+
|
|
1668
|
+
Uma reaction publica com `ctx.emit` somente os eventos listados no seu `emits`. Qualquer outro evento, inclusive de reaction sem `emits`, é descartado com o aviso `runtime.reaction_emit_ignored`, controlado por `config.warnUndeclaredEmits`. O evento publicado herda `actor`, `tenant` e `source` do evento recebido e ganha `cause: { reaction, eventId, eventType, depth }`.
|
|
1669
|
+
|
|
1670
|
+
Uma cadeia dá no máximo `config.maxReactionDepth` saltos (default 5). O `ctx.emit` que passaria do limite não publica e registra o erro `runtime.reaction_chain_too_deep`; com `emitMode: 'strict'`, o mesmo código interrompe a reaction. Falha de `publish` segue `emitMode`, como nas actions. Ver ADR 0027.
|
|
1641
1671
|
|
|
1642
1672
|
### Setup unificado
|
|
1643
1673
|
|
|
@@ -1663,20 +1693,20 @@ await runtime.start()
|
|
|
1663
1693
|
|
|
1664
1694
|
### Resiliência: quem implementa o quê
|
|
1665
1695
|
|
|
1666
|
-
| Campo | Implementação
|
|
1667
|
-
|
|
1668
|
-
| `dedup` |
|
|
1669
|
-
| `retry` |
|
|
1670
|
-
| `timeout` |
|
|
1671
|
-
| `concurrency` |
|
|
1696
|
+
| Campo | Implementação no core (ADR 0026) |
|
|
1697
|
+
|---------------|----------------------------------|
|
|
1698
|
+
| `dedup` | Chave por reaction, em memória: ocupada da chegada até o fim da entrega e, após sucesso, por `config.dedupCacheTtl` (default 60 s), com no máximo `config.dedupCacheMaxEntries` chaves concluídas por reaction (default 10.000; a mais antiga sai primeiro). Falha definitiva libera a chave. Duplicata vira `debug` `runtime.reaction_duplicate_skipped`. |
|
|
1699
|
+
| `retry` | Em memória. Sem `retry`, uma execução (`config.defaultRetry` vale só para background). Exceções comuns e timeout repetem. `ActionError` segue `retriable`, cujo padrão vem da categoria: só `rate_limit` e `dependency` repetem (`internal` não); `error({ ..., retriable: true })` força a repetição. Cada repetição avisa `runtime.reaction_retry`. |
|
|
1700
|
+
| `timeout` | `AbortController` por tentativa: aborta `ctx.signal` e falha com `runtime.reaction_timeout` (retriable). A vaga é liberada no timeout. |
|
|
1701
|
+
| `concurrency` | Semáforo por reaction (default 1) e global (`config.maxConcurrentReactions`, default 10), em ordem de chegada. A espera entre tentativas não ocupa vaga. |
|
|
1672
1702
|
|
|
1673
|
-
|
|
1703
|
+
As políticas valem por processo. Delegar retry e concorrência a um adapter durável (consumer groups, filas) fica para quando existir driver distribuído. Valores inválidos falham no registro com `runtime.invalid_reaction_policy` ou na criação do runtime com `runtime.invalid_config`.
|
|
1674
1704
|
|
|
1675
1705
|
### Falha de reaction não afeta o emissor
|
|
1676
1706
|
|
|
1677
1707
|
Reaction roda **assíncrono** ao handler que emitiu o evento. Se reaction falha, é problema dela (retry / dead-letter via adapter); a action que emitiu o evento já completou.
|
|
1678
1708
|
|
|
1679
|
-
Falhas registradas no log do runtime
|
|
1709
|
+
Falhas definitivas são registradas no log do runtime (`reaction failed`, com o número de tentativas) e no audit (registro com `outcome: 'error'`). Adapter de eventos pode rotear pra dead-letter (v1).
|
|
1680
1710
|
|
|
1681
1711
|
### Pipeline de uma reaction
|
|
1682
1712
|
|
|
@@ -1684,14 +1714,14 @@ Falhas registradas no log do runtime + audit (entrada do tipo `'reaction.failed'
|
|
|
1684
1714
|
EventBusAdapter recebe evento (publish externo ou ctx.emit interno)
|
|
1685
1715
|
↓
|
|
1686
1716
|
matching: encontra reactions registradas pra esse padrão
|
|
1687
|
-
↓ (paralelo, com concurrency limit)
|
|
1688
1717
|
para cada reaction:
|
|
1689
|
-
↓ (a) dedup
|
|
1690
|
-
↓
|
|
1718
|
+
↓ (a) dedup: reserva a chave na chegada (se declarado)
|
|
1719
|
+
↓ espera vaga da reaction e vaga global (concurrency)
|
|
1720
|
+
↓ (b) authorize, uma vez (se declarado)
|
|
1691
1721
|
↓ (c) handler executa (com timeout)
|
|
1692
|
-
↓ (d) ctx.emit
|
|
1693
|
-
↓ (e)
|
|
1694
|
-
↓ (f)
|
|
1722
|
+
↓ (d) ctx.emit publica os eventos declarados em `emits`, até a profundidade máxima
|
|
1723
|
+
↓ (e) retry se falhou e a política permite (libera as vagas durante a espera)
|
|
1724
|
+
↓ (f) audit, um registro por entrega (completed/failed, com as tentativas)
|
|
1695
1725
|
```
|
|
1696
1726
|
|
|
1697
1727
|
### O que **não** entra em v0
|
|
@@ -1758,6 +1788,7 @@ type RuntimeConfig = {
|
|
|
1758
1788
|
|
|
1759
1789
|
// — Resiliência / dedup —
|
|
1760
1790
|
dedupCacheTtl?: number // ms, default 60_000
|
|
1791
|
+
dedupCacheMaxEntries?: number // chaves concluídas por reaction, default 10_000
|
|
1761
1792
|
defaultRetry?: { attempts: number, backoff?: BackoffSpec }
|
|
1762
1793
|
|
|
1763
1794
|
// — i18n —
|
|
@@ -1768,7 +1799,8 @@ type RuntimeConfig = {
|
|
|
1768
1799
|
|
|
1769
1800
|
// — Limites —
|
|
1770
1801
|
maxConcurrentActions?: number // default Infinity
|
|
1771
|
-
maxConcurrentReactions?: number // default 10
|
|
1802
|
+
maxConcurrentReactions?: number // default 10, somando todas as reactions
|
|
1803
|
+
maxReactionDepth?: number // default 5 saltos de reaction por cadeia
|
|
1772
1804
|
}
|
|
1773
1805
|
```
|
|
1774
1806
|
|
|
@@ -1831,9 +1863,11 @@ Se você não passa `config`, o runtime usa:
|
|
|
1831
1863
|
validateOutputInDev: true,
|
|
1832
1864
|
warnUndeclaredEmits: true,
|
|
1833
1865
|
dedupCacheTtl: 60_000,
|
|
1866
|
+
dedupCacheMaxEntries: 10_000,
|
|
1834
1867
|
defaultRetry: { attempts: 3, backoff: { kind: 'exponential', initialMs: 1000 } },
|
|
1835
1868
|
maxConcurrentActions: Number.POSITIVE_INFINITY,
|
|
1836
1869
|
maxConcurrentReactions: 10,
|
|
1870
|
+
maxReactionDepth: 5,
|
|
1837
1871
|
}
|
|
1838
1872
|
```
|
|
1839
1873
|
|
|
@@ -2134,12 +2168,14 @@ chamada, inclusive por IA e MCP.
|
|
|
2134
2168
|
- **`QueueAdapter`** — adapter que executa background jobs. Ver §8/§10.
|
|
2135
2169
|
- **`ctx.emit`** — função disponível no contexto do handler para emitir eventos declarados em `emits`. Ver §11.
|
|
2136
2170
|
- **`requires`** — permissão declarativa (string ou array) usada por adapters de docs/OpenAPI. Distinta de `authorize` (lógica runtime). Ver §4.
|
|
2137
|
-
- **`automatable`** —
|
|
2171
|
+
- **`automatable`** — opt-in que permite a um schedule executar a action como o sistema, sem usuário. Negado por padrão; não abre a action a anônimos. Distinta de `ai.enabled`. Ver §4.
|
|
2172
|
+
- **`isSystemActor(ctx)`** — responde se a execução foi criada pelo runtime para o sistema (schedule ou job que ele enfileirou). Usado no `authorize` de actions `automatable`. Ver §4.
|
|
2138
2173
|
- **`internal`** — flag que indica action service-to-service apenas; server adapter não expõe como REST público. Ver §2.
|
|
2139
2174
|
- **Reaction** — declaração que escuta evento(s) e executa handler. Auto-registrada pelo runtime via EventBusAdapter. Pareada com Action (ator inicia × sistema reage). Ver §12.
|
|
2140
2175
|
- **`defineReaction`** — função que constrói uma reaction declarativa.
|
|
2141
2176
|
- **`ReactionContext`** — contexto entregue ao handler de reaction (user, tenant, db, emit, log, meta).
|
|
2142
|
-
- **`dedup`** — função em reaction que produz chave de idempotência; runtime descarta evento
|
|
2177
|
+
- **`dedup`** — função em reaction que produz chave de idempotência; runtime descarta evento com chave em processamento ou processada dentro de `dedupCacheTtl`. Ver §12.
|
|
2178
|
+
- **`cause`** — campo de `DomainEvent` publicado por reaction: reaction, evento de origem e profundidade na cadeia. Ver §11/§12.
|
|
2143
2179
|
- **`LoggerAdapter`** — interface de logger plugável (levels, structured, child binding). Default: `@softize/opus/log-console`. Ver §8.
|
|
2144
2180
|
- **`RuntimeConfig`** — behavior knobs do runtime (auditMode, emitMode, env, i18n, limits). Recebe valores literais; core nunca lê env direto. Ver §14.
|
|
2145
2181
|
- **`healthCheck()`** — método opcional em qualquer adapter, retorna `{ ok, details? }`. Agregado por `runtime.healthCheck()` e exposto em `/ready`. Ver §8.
|
package/package.json
CHANGED
package/src/core/audit.ts
CHANGED
|
@@ -61,7 +61,8 @@ export class AuditEmitter {
|
|
|
61
61
|
if (targetSinks.length === 0) return
|
|
62
62
|
|
|
63
63
|
const results = await Promise.allSettled(
|
|
64
|
-
|
|
64
|
+
// `then` também captura o sink que lança de forma síncrona: toda falha vira rejeição tratada.
|
|
65
|
+
targetSinks.map((sink) => Promise.resolve().then(() => sink.emit(processed))),
|
|
65
66
|
)
|
|
66
67
|
|
|
67
68
|
const failures = results.filter(
|
package/src/core/index.ts
CHANGED
|
@@ -216,7 +216,7 @@ export type {
|
|
|
216
216
|
export { AuditEmitter } from './audit.ts'
|
|
217
217
|
|
|
218
218
|
// — Runtime ———————————————————————————————————————————————————————————————————
|
|
219
|
-
export { Runtime, createRuntime } from './runtime.ts'
|
|
219
|
+
export { Runtime, createRuntime, isSystemActor } from './runtime.ts'
|
|
220
220
|
export type {
|
|
221
221
|
RuntimeSetup,
|
|
222
222
|
RuntimeSetupConfig,
|
package/src/core/reactions.ts
CHANGED
|
@@ -27,7 +27,7 @@ export function defineReaction<Event = unknown>(
|
|
|
27
27
|
// =============================================================================
|
|
28
28
|
|
|
29
29
|
/** Campos server-only que o `bindReaction` fornece — o resto é a DECLARAÇÃO (isomorfa). */
|
|
30
|
-
type ReactionBindingKeys = 'handler' | 'retry' | 'timeout' | 'dedup' | 'concurrency'
|
|
30
|
+
type ReactionBindingKeys = 'handler' | 'emits' | 'retry' | 'timeout' | 'dedup' | 'concurrency'
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* A DECLARAÇÃO de uma reação, autorável na sessão de design (isomorfa, SEM efeito): qual
|
|
@@ -36,9 +36,13 @@ type ReactionBindingKeys = 'handler' | 'retry' | 'timeout' | 'dedup' | 'concurre
|
|
|
36
36
|
*/
|
|
37
37
|
export type ReactionContract<Event = unknown> = Omit<ReactionDef<Event>, ReactionBindingKeys>
|
|
38
38
|
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Parte server-only amarrada por `bindReaction`: o handler, os eventos que ele publica e a
|
|
41
|
+
* política de entrega. Como no `bindAction`, `emits` fica no binding, ao lado do handler.
|
|
42
|
+
*/
|
|
40
43
|
export interface ReactionBinding<Event = unknown> {
|
|
41
44
|
handler: ReactionHandlerFn<Event>
|
|
45
|
+
emits?: string[]
|
|
42
46
|
retry?: { attempts: number; backoff?: BackoffSpec }
|
|
43
47
|
timeout?: number
|
|
44
48
|
dedup?: (event: DomainEvent<Event>) => string
|