@softize/opus 18.5.0 → 20.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/check.mjs +198 -10
  4. package/bin/lib/copy.mjs +29 -0
  5. package/bin/lib/mcp.mjs +1 -1
  6. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
  7. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  8. package/docs/data-products.md +15 -9
  9. package/docs/protocol.md +3 -4
  10. package/docs/releasing.md +2 -2
  11. package/package.json +2 -2
  12. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  13. package/registry/templates/app/package.json +1 -1
  14. package/src/core/runtime.ts +96 -8
  15. package/src/core/types.ts +5 -2
  16. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  17. package/src/ui/components/patterns/content-header.tsx +16 -12
  18. package/src/ui/components/patterns/fill-region.ts +150 -0
  19. package/src/ui/components/patterns/heading-level.ts +16 -0
  20. package/src/ui/components/patterns/list.tsx +103 -73
  21. package/src/ui/components/patterns/page.tsx +20 -4
  22. package/src/ui/components/patterns/pane.tsx +294 -0
  23. package/src/ui/components/patterns/presentation.tsx +81 -54
  24. package/src/ui/components/patterns/sidebar.tsx +13 -72
  25. package/src/ui/components/patterns/split.tsx +51 -38
  26. package/src/ui/components/patterns/surface-anatomy.ts +246 -0
  27. package/src/ui/components/patterns/surface-header.tsx +51 -6
  28. package/src/ui/components/patterns/surface-media.tsx +69 -0
  29. package/src/ui/components/patterns/trigger.tsx +12 -2
  30. package/src/ui/components/primitives/alert.tsx +9 -1
  31. package/src/ui/components/primitives/avatar.tsx +7 -3
  32. package/src/ui/components/primitives/card.tsx +91 -29
  33. package/src/ui/components/primitives/carousel.tsx +34 -5
  34. package/src/ui/components/primitives/checkbox.tsx +11 -3
  35. package/src/ui/components/primitives/command.tsx +14 -5
  36. package/src/ui/components/primitives/copyable.tsx +120 -10
  37. package/src/ui/components/primitives/detail.tsx +10 -13
  38. package/src/ui/components/primitives/dialog.tsx +179 -45
  39. package/src/ui/components/primitives/drawer.tsx +131 -24
  40. package/src/ui/components/primitives/empty.tsx +1 -1
  41. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  42. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  43. package/src/ui/components/primitives/input-group.tsx +9 -2
  44. package/src/ui/components/primitives/input-otp.tsx +1 -1
  45. package/src/ui/components/primitives/kbd.tsx +1 -1
  46. package/src/ui/components/primitives/label.tsx +3 -0
  47. package/src/ui/components/primitives/menu.tsx +5 -0
  48. package/src/ui/components/primitives/metric-card.tsx +98 -39
  49. package/src/ui/components/primitives/popover.tsx +1 -1
  50. package/src/ui/components/primitives/progress.tsx +9 -1
  51. package/src/ui/components/primitives/slider.tsx +62 -11
  52. package/src/ui/components/primitives/truncate.tsx +10 -1
  53. package/src/ui/docs/content/accordion.md +74 -27
  54. package/src/ui/docs/content/action-form-card.md +24 -15
  55. package/src/ui/docs/content/action-form-dialog.md +17 -7
  56. package/src/ui/docs/content/action-form.md +92 -77
  57. package/src/ui/docs/content/action-list-dialog.md +75 -38
  58. package/src/ui/docs/content/action-list.md +134 -52
  59. package/src/ui/docs/content/action-trigger.md +27 -10
  60. package/src/ui/docs/content/action-view.md +14 -14
  61. package/src/ui/docs/content/actions.md +161 -66
  62. package/src/ui/docs/content/ai.md +43 -44
  63. package/src/ui/docs/content/alert.md +83 -29
  64. package/src/ui/docs/content/ask.md +51 -26
  65. package/src/ui/docs/content/aspect-ratio.md +22 -24
  66. package/src/ui/docs/content/audit.md +23 -19
  67. package/src/ui/docs/content/auth.md +38 -19
  68. package/src/ui/docs/content/avatar.md +54 -5
  69. package/src/ui/docs/content/badge.md +29 -9
  70. package/src/ui/docs/content/breadcrumb.md +21 -8
  71. package/src/ui/docs/content/button.md +73 -43
  72. package/src/ui/docs/content/calendar.md +134 -26
  73. package/src/ui/docs/content/card.md +252 -35
  74. package/src/ui/docs/content/carousel.md +37 -6
  75. package/src/ui/docs/content/chat.md +118 -37
  76. package/src/ui/docs/content/checkbox.md +73 -7
  77. package/src/ui/docs/content/cli.md +94 -55
  78. package/src/ui/docs/content/collapsible.md +43 -19
  79. package/src/ui/docs/content/command.md +30 -8
  80. package/src/ui/docs/content/composer.md +23 -8
  81. package/src/ui/docs/content/content.md +37 -7
  82. package/src/ui/docs/content/copyable.md +39 -8
  83. package/src/ui/docs/content/customization.md +55 -46
  84. package/src/ui/docs/content/cycle.md +33 -18
  85. package/src/ui/docs/content/data-products.md +115 -0
  86. package/src/ui/docs/content/data-state.md +20 -6
  87. package/src/ui/docs/content/data.md +20 -19
  88. package/src/ui/docs/content/detail.md +12 -5
  89. package/src/ui/docs/content/dialog.md +219 -28
  90. package/src/ui/docs/content/dictionary-value.md +4 -4
  91. package/src/ui/docs/content/dock.md +54 -19
  92. package/src/ui/docs/content/dot.md +35 -5
  93. package/src/ui/docs/content/drawer.md +193 -22
  94. package/src/ui/docs/content/empty-value.md +20 -6
  95. package/src/ui/docs/content/empty.md +24 -3
  96. package/src/ui/docs/content/events.md +76 -16
  97. package/src/ui/docs/content/executions.md +9 -9
  98. package/src/ui/docs/content/field.md +21 -6
  99. package/src/ui/docs/content/getting-started.md +44 -34
  100. package/src/ui/docs/content/icon-picker.md +19 -13
  101. package/src/ui/docs/content/icon-tile.md +28 -5
  102. package/src/ui/docs/content/input-otp.md +58 -7
  103. package/src/ui/docs/content/input.md +22 -3
  104. package/src/ui/docs/content/item.md +53 -6
  105. package/src/ui/docs/content/kbd.md +5 -1
  106. package/src/ui/docs/content/label.md +13 -4
  107. package/src/ui/docs/content/log.md +5 -5
  108. package/src/ui/docs/content/markdown.md +5 -4
  109. package/src/ui/docs/content/mcp.md +49 -23
  110. package/src/ui/docs/content/menu.md +95 -10
  111. package/src/ui/docs/content/metric-card.md +68 -21
  112. package/src/ui/docs/content/observability.md +10 -9
  113. package/src/ui/docs/content/page.md +125 -11
  114. package/src/ui/docs/content/pagination.md +13 -5
  115. package/src/ui/docs/content/pane.md +300 -0
  116. package/src/ui/docs/content/popover.md +42 -5
  117. package/src/ui/docs/content/presentation.md +91 -47
  118. package/src/ui/docs/content/progress.md +27 -14
  119. package/src/ui/docs/content/queue.md +39 -21
  120. package/src/ui/docs/content/radio-group.md +26 -4
  121. package/src/ui/docs/content/router.md +29 -10
  122. package/src/ui/docs/content/runtime.md +90 -54
  123. package/src/ui/docs/content/scheduler.md +53 -21
  124. package/src/ui/docs/content/scroll-area.md +42 -35
  125. package/src/ui/docs/content/select.md +78 -33
  126. package/src/ui/docs/content/semantic-context.md +21 -2
  127. package/src/ui/docs/content/separator.md +15 -9
  128. package/src/ui/docs/content/sidebar.md +25 -45
  129. package/src/ui/docs/content/skeleton.md +13 -4
  130. package/src/ui/docs/content/slider.md +37 -9
  131. package/src/ui/docs/content/spinner.md +4 -3
  132. package/src/ui/docs/content/split.md +34 -20
  133. package/src/ui/docs/content/storage.md +8 -9
  134. package/src/ui/docs/content/switch.md +10 -7
  135. package/src/ui/docs/content/table.md +46 -8
  136. package/src/ui/docs/content/tabs.md +14 -4
  137. package/src/ui/docs/content/testing.md +76 -31
  138. package/src/ui/docs/content/textarea.md +5 -4
  139. package/src/ui/docs/content/toast.md +48 -7
  140. package/src/ui/docs/content/toggle.md +3 -3
  141. package/src/ui/docs/content/tokens.md +15 -26
  142. package/src/ui/docs/content/tooltip.md +18 -2
  143. package/src/ui/docs/content/truncate.md +36 -6
  144. package/src/ui/docs/content/ui.md +74 -22
  145. package/src/ui/docs/content/upgrading.md +45 -36
  146. package/src/ui/docs/plugin.ts +36 -2
  147. package/src/ui/docs/registry.tsx +14 -2
  148. package/src/ui/drivers/react.tsx +16 -8
  149. package/src/ui/meta.ts +38 -32
  150. package/src/ui/react.tsx +14 -0
  151. package/src/vite/design.ts +16 -4
@@ -1,18 +1,26 @@
1
1
  ## Progresso determinado
2
2
 
3
- Use `Progress` quando a tarefa informar uma porcentagem de conclusão. `value` aceita valores de 0 a
4
- 100 e anima o preenchimento a cada mudança. Sem valor, a barra permanece vazia.
3
+ Use `Progress` quando a tarefa informar quanto já foi concluído. `value` aceita valores de 0 a
4
+ `max`, que por padrão é 100, e anima o preenchimento a cada mudança. Para contar etapas, declare
5
+ `max` com o total: `value={3} max={4}` preenche três quartos da barra. Quando a duração da espera
6
+ não é conhecida, use [`Spinner`](/ui/spinner).
7
+
8
+ A barra é exposta aos leitores de tela como `progressbar`, com o valor atual, mas o texto ao lado
9
+ dela não a nomeia automaticamente. Ligue o rótulo visível à barra com `aria-labelledby` ou, sem
10
+ rótulo visível, passe `aria-label`.
5
11
 
6
12
  ```tsx preview col
7
13
  <div className="w-full max-w-sm space-y-2">
8
14
  <div className="flex items-center justify-between text-sm">
9
- <span>opus check</span>
15
+ <span id="progress-check">opus check</span>
10
16
  <span className="text-muted-foreground">65%</span>
11
17
  </div>
12
- <Progress value={65} />
18
+ <Progress value={65} aria-labelledby="progress-check" />
13
19
  </div>
14
20
  ```
15
21
 
22
+ Sem `value`, ou com `null`, a barra fica vazia e é exposta como progresso indeterminado.
23
+
16
24
  ## Controlado
17
25
 
18
26
  Controle `value` externamente quando o progresso acompanhar uma tarefa em andamento. No exemplo,
@@ -20,15 +28,16 @@ cada clique avança uma etapa da sincronização.
20
28
 
21
29
  ```tsx preview col
22
30
  const [step, setStep] = useState(40)
31
+ const labelId = useId()
23
32
  const advance = (): void => setStep((prev) => Math.min(prev + 20, 100))
24
33
 
25
34
  render(
26
35
  <div className="w-full max-w-sm space-y-3">
27
36
  <div className="flex items-center justify-between text-sm">
28
- <span>Sincronizar Empresa X</span>
37
+ <span id={labelId}>Sincronizar Empresa X</span>
29
38
  <span className="text-muted-foreground">{step}%</span>
30
39
  </div>
31
- <Progress value={step} />
40
+ <Progress value={step} aria-labelledby={labelId} />
32
41
  <Button variant="outline" size="sm" onClick={advance}>
33
42
  Avançar etapa
34
43
  </Button>
@@ -39,18 +48,19 @@ render(
39
48
  ## Altura e contexto
40
49
 
41
50
  `context` diz o que o andamento comunica — `success` para meta atingida, `warning` ou `danger`
42
- para atenção — e tinge trilha e preenchimento com a família semântica. `className` compõe sobre o
43
- padrão para ajustar a altura.
51
+ para atenção — e tinge trilha e preenchimento com a família semântica. A cor reforça o significado,
52
+ mas não o substitui: mantenha o rótulo e o valor legíveis. `className` compõe sobre o padrão para
53
+ ajustar a altura.
44
54
 
45
55
  ```tsx preview col
46
56
  <div className="w-full max-w-sm space-y-4">
47
57
  <div className="space-y-1.5">
48
- <span className="text-sm">Cobertura de testes</span>
49
- <Progress value={92} context="success" className="h-1.5" />
58
+ <span id="progress-coverage" className="text-sm">Cobertura de testes</span>
59
+ <Progress value={92} context="success" className="h-1.5" aria-labelledby="progress-coverage" />
50
60
  </div>
51
61
  <div className="space-y-1.5">
52
- <span className="text-sm">Skills cobertas pelo revisor</span>
53
- <Progress value={45} context="warning" className="h-3" />
62
+ <span id="progress-review" className="text-sm">Skills cobertas pelo revisor</span>
63
+ <Progress value={45} context="warning" className="h-3" aria-labelledby="progress-review" />
54
64
  </div>
55
65
  </div>
56
66
  ```
@@ -59,6 +69,9 @@ padrão para ajustar a altura.
59
69
 
60
70
  | Propriedade | Tipo | Padrão | Descrição |
61
71
  |---|---|---|---|
62
- | `value` | `number \| null` | | O progresso de 0 a 100. O preenchimento anima a cada mudança; null/ausente deixa a barra vazia. |
72
+ | `value` | `number \| null` | | O progresso de 0 a `max`. O preenchimento anima a cada mudança; sem valor, a barra fica vazia e indeterminada. |
73
+ | `max` | `number` | `100` | O valor que representa a conclusão. O preenchimento é a fração `value / max`. |
63
74
  | `context` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'primary'` | O que o andamento comunica; tinge trilha e preenchimento pela família `--context-*`. |
64
- | `className` | `string` | | Compõe sobre o padrão — ajuste a altura (h-1.5/h-3). |
75
+ | `aria-label` | `string` | | Nome acessível da barra quando não há rótulo visível. |
76
+ | `aria-labelledby` | `string` | | `id` do rótulo visível que nomeia a barra. |
77
+ | `className` | `string` | | Compõe sobre o padrão — ajuste a altura (`h-1.5`, `h-3`). |
@@ -5,7 +5,7 @@ title: Filas
5
5
  # Filas
6
6
 
7
7
  Use uma action em background quando a operação não puder manter a requisição aberta. O
8
- `QueueAdapter` enfileira o trabalho e devolve um `JobHandle` para acompanhamento pelo cliente.
8
+ `QueueAdapter` enfileira o trabalho e devolve um `JobHandle` para acompanhar o andamento.
9
9
 
10
10
  ## O contrato
11
11
 
@@ -21,18 +21,21 @@ interface JobHandle<T = unknown> {
21
21
  jobId: string
22
22
  action: string
23
23
  status: 'queued' | 'running' | 'done' | 'failed' | 'cancelled'
24
- progress?: { percent?: number; message?: string }
24
+ progress?: { percent?: number; message?: string; data?: unknown }
25
25
  data?: T
26
26
  error?: ActionError
27
+ enqueuedAt: string
28
+ startedAt?: string
29
+ finishedAt?: string
27
30
  attempts: number
28
31
  trace?: TraceContext
29
32
  }
30
33
  ```
31
34
 
32
- `runtime.execute()` valida a chamada, cria o `JobSpec` e inclui o trace efetivo, request ID e
33
- provenance original. O adapter persiste o envelope; o handle também expõe esse trace. No
34
- worker, `runtime.executeJob()` revalida o envelope e executa um span filho, preservando a
35
- cadeia request → enqueue → job sem aceitar baggage ou campos extras no carrier.
35
+ `runtime.execute()` valida a chamada, cria o `JobSpec` e inclui o trace efetivo, o request ID e a
36
+ provenance original. O adapter persiste esse envelope, e o handle também expõe o trace. No worker,
37
+ `runtime.executeJob()` revalida o envelope e executa um span filho, preservando a cadeia requisição
38
+ → enfileiramento → job sem aceitar baggage ou campos extras.
36
39
 
37
40
  ## Driver
38
41
 
@@ -43,26 +46,41 @@ import { bullmqQueue } from '@softize/opus/queue/bullmq'
43
46
  const queue = bullmqQueue({ queue: new Queue('opus', { connection: { host: 'localhost' } }) })
44
47
  ```
45
48
 
46
- Redis-backed (durável, entre-processos, retries). Peers opcionais (`bullmq`, `ioredis`) só
47
- para quem usa o driver.
49
+ O driver usa Redis por meio do BullMQ, com durabilidade, entrega entre processos e novas tentativas.
50
+ `bullmq` é dependência opcional: só precisa ser instalado por quem usa o driver.
48
51
 
49
- ## No runtime
52
+ ## Declarar a action em background
53
+
54
+ Configure a fila no runtime e declare `background` no binding da action:
50
55
 
51
56
  ```ts
52
57
  const runtime = createRuntime({
53
58
  // …server/data…
54
59
  queue,
55
60
  })
61
+
62
+ export const relatorioGerar = bindAction(relatorioGerarContract, {
63
+ background: { enabled: true, retry: { attempts: 3 }, progress: true },
64
+ handler: async (ctx, input, loaded, progress) => {
65
+ await progress?.report({ percent: 50, message: 'Consultando dados' })
66
+ return gerarRelatorio(input)
67
+ },
68
+ })
56
69
  ```
57
70
 
58
- A action declara `background: { enabled: true }` (+ retry/priority no contrato); chamá-la devolve um
59
- `JobHandle` em vez do resultado, e o cliente pola `status(jobId)` (ou `subscribe`) até
60
- `done`/`failed`.
71
+ Só actions `simple` e `form` executam em background; além de `retry` e `progress`, a configuração
72
+ aceita `queue`, `priority` e `timeout`. Em uma action `list` ou `view`, o tipo do binding aceita
73
+ `background`, mas o runtime o ignora e executa a action na própria requisição. Para evitar essa
74
+ surpresa, o runtime avisa ao registrar a action fora de produção (`runtime.action_field_ignored`), e
75
+ o [`opus check`](/docs/cli) aponta a regra `ignored-by-kind`. Sem `QueueAdapter`, a chamada falha com
76
+ `runtime.queue_required`. Chamar a action devolve um `JobHandle` em vez do resultado. O Opus não
77
+ monta uma rota de acompanhamento: exponha `queue.status(jobId)` ou `queue.subscribe` por uma action
78
+ ou rota do próprio app até o job chegar a `done` ou `failed`.
61
79
 
62
80
  ## Worker BullMQ
63
81
 
64
- O app continua dono do `Worker` e da reidratação de autenticação. O processor chama
65
- `executeJob()` — nunca `execute()`, que criaria outro job:
82
+ O app continua dono do `Worker` e de reconstruir a autenticação de quem enfileirou. O processor
83
+ chama `executeJob()`, e não `execute()`, que criaria outro job:
66
84
 
67
85
  ```ts
68
86
  new Worker('opus', async (job) => {
@@ -76,13 +94,13 @@ new Worker('opus', async (job) => {
76
94
  }, { connection })
77
95
  ```
78
96
 
79
- O runtime confirma que o actor reidratado corresponde a `spec.ctx.userId`; tenant, request,
80
- trace e provenance vêm do envelope. O driver mapeia `attempts`, backoff nativo e prioridade.
81
- Multiplicador exponencial diferente de 2 ou `maxMs` exige `backoffStrategy` custom do worker e
82
- é rejeitado pelo driver em vez de ser silenciosamente ignorado. `timeout` permanece no
83
- envelope para o worker aplicar, pois não é uma opção de execução do `Queue.add`.
97
+ O runtime confirma que o usuário reconstruído corresponde a `spec.ctx.userId`; tenant, request,
98
+ trace e provenance vêm do envelope. O driver traduz `attempts`, o backoff nativo e a prioridade para
99
+ o BullMQ. Um backoff exponencial com multiplicador diferente de 2 ou com `maxMs` exige uma
100
+ `backoffStrategy` própria no worker, e o driver o recusa em vez de ignorá-lo em silêncio. `timeout`
101
+ permanece no envelope para o worker aplicar, porque não é uma opção de execução do `Queue.add`.
84
102
 
85
103
  ## Limites atuais
86
104
 
87
- Driver hoje: `bullmq` (Redis). In-memory para dev e outros backends (SQS, pg-boss…) entram
88
- por reincidência.
105
+ O único driver é o `bullmq`. Uma fila em memória para desenvolvimento e outros backends, como SQS e
106
+ pg-boss, ainda não existem.
@@ -1,8 +1,10 @@
1
1
  ## Escolha única
2
2
 
3
- Use `RadioGroup` quando todas as opções mutuamente exclusivas precisarem permanecer visíveis. Cada
4
- `RadioGroupItem` declara um `value` e deve estar associado a um `Label`. `defaultValue` define a
5
- opção inicial no modo não controlado.
3
+ Use `RadioGroup` quando todas as opções mutuamente exclusivas precisarem permanecer visíveis. Com
4
+ muitas opções, prefira [`Select`](/ui/select); para ligar ou desligar um único item, use
5
+ [`Checkbox`](/ui/checkbox) ou [`Switch`](/ui/switch). Cada `RadioGroupItem` declara um `value` e
6
+ deve estar associado a um `Label`, escrito logo depois do item. `defaultValue` define a opção
7
+ inicial no modo não controlado.
6
8
 
7
9
  ```tsx preview col-start
8
10
  <RadioGroup defaultValue="balanced">
@@ -68,6 +70,25 @@ bloquear todas as opções, aplique `disabled` ao `RadioGroup`.
68
70
  </RadioGroup>
69
71
  ```
70
72
 
73
+ ## Estado inválido
74
+
75
+ Quando a escolha for obrigatória e nenhuma opção estiver marcada, declare `aria-invalid` em cada
76
+ `RadioGroupItem` e explique o que falta em um texto associado ao grupo por `aria-describedby`.
77
+
78
+ ```tsx preview col-start
79
+ <RadioGroup aria-describedby="review-mode-error">
80
+ <div className="flex items-center gap-2">
81
+ <RadioGroupItem id="review-auto" value="auto" aria-invalid />
82
+ <Label htmlFor="review-auto">Revisar automaticamente</Label>
83
+ </div>
84
+ <div className="flex items-center gap-2">
85
+ <RadioGroupItem id="review-manual" value="manual" aria-invalid />
86
+ <Label htmlFor="review-manual">Revisar manualmente</Label>
87
+ </div>
88
+ </RadioGroup>
89
+ <FieldError id="review-mode-error">Escolha como a sessão será revisada.</FieldError>
90
+ ```
91
+
71
92
  ## Propriedades de RadioGroup
72
93
 
73
94
  | Propriedade | Tipo | Padrão | Descrição |
@@ -82,4 +103,5 @@ bloquear todas as opções, aplique `disabled` ao `RadioGroup`.
82
103
  | Propriedade | Tipo | Padrão | Descrição |
83
104
  |---|---|---|---|
84
105
  | `value` | `string` | | Valor que o item atribui ao grupo quando selecionado. |
85
- | `disabled` | `boolean` | `false` | Desabilita somente este item; o `Label` associado acompanha o estado. |
106
+ | `disabled` | `boolean` | `false` | Desabilita somente este item; o `Label` escrito logo depois dele acompanha o estado. |
107
+ | `aria-invalid` | `boolean` | | Aplica a borda e o anel de erro ao item. |
@@ -19,11 +19,13 @@ function App() {
19
19
  O router oferece leitura reativa da URL e `navigate`; o aplicativo decide o que renderizar. Ele não
20
20
  inclui tabela de rotas, parâmetros tipados nem carregamento de dados.
21
21
 
22
- A régua: se a sua tela precisa de casamento de padrão (`/users/:id/posts/:postId`), params tipados ou data loaders, o caso pede uma biblioteca de rotas, não isto. Um app de back-office com uma dúzia de destinos quase nunca precisa.
22
+ Se a tela precisa casar padrões como `/users/:id/posts/:postId`, receber parâmetros tipados ou
23
+ carregar dados por rota, use uma biblioteca de rotas. Um app de back-office com uma dúzia de
24
+ destinos costuma dispensar esses recursos.
23
25
 
24
26
  ## As quatro peças
25
27
 
26
- | | |
28
+ | API | Comportamento |
27
29
  |---|---|
28
30
  | `usePathname()` | O pathname como estado reativo. |
29
31
  | `useSegments()` | Os segmentos, já sem os vazios: `/settings/users` → `['settings','users']`. |
@@ -32,15 +34,23 @@ A régua: se a sua tela precisa de casamento de padrão (`/users/:id/posts/:post
32
34
 
33
35
  ## Comportamentos preservados
34
36
 
35
- **Destino igual é no-op.** `navigate` resolve o destino com `new URL` e compara o `href` inteiro — não faz nada se você já está exatamente lá. Sem isso, clicar duas vezes no mesmo item do menu empilha entradas idênticas e o botão "voltar" não sai do lugar.
37
+ **Navegar para o destino atual não cria entrada.** `navigate` resolve o destino com `new URL` e
38
+ compara o `href` inteiro; se a pessoa já está exatamente lá, nada acontece. Assim, clicar duas vezes
39
+ no mesmo item do menu não empilha entradas idênticas, e o botão Voltar continua saindo da página.
36
40
 
37
- Comparar strings cruas parece bastar e não basta, em duas frentes. O **hash**: estando em `/a#secao`, `navigate('/a')` pareceria destino repetido e a âncora nunca sairia da URL. E o **encoding**: `window.location` devolve `/relatórios` como `/relat%C3%B3rios`, então a comparação crua nunca casa e cada clique empilha — bem no caso pt-BR, e no ``navigate(`/reports?report=${arquivo}`)`` com nome de arquivo acentuado.
41
+ A comparação usa a URL resolvida, e não o texto cru, por dois motivos. O hash: estando em
42
+ `/a#secao`, `navigate('/a')` precisa remover a âncora, e não ser tratado como destino repetido. E a
43
+ codificação: `window.location` devolve `/relatórios` como `/relat%C3%B3rios`, então uma comparação
44
+ crua nunca casaria e cada clique empilharia uma entrada, o que afeta caminhos acentuados e
45
+ chamadas como ``navigate(`/reports?report=${arquivo}`)`` com nome de arquivo acentuado.
38
46
 
39
47
  **A notificação usa um evento do navegador.** Como `pushState` não dispara `popstate`, `navigate`
40
48
  emite o evento explicitamente. Isso mantém listeners existentes e cópias diferentes do pacote
41
49
  sincronizados pela mesma janela.
42
50
 
43
- **`useSyncExternalStore`, não `useState`.** Ler `window.location` dentro de `useState`/`useEffect` sofre *tearing* no modo concurrent: dois componentes podem renderizar o mesmo commit com URLs diferentes. As cópias que este módulo substituiu faziam isso.
51
+ **Todos os componentes leem a mesma URL.** Os hooks usam `useSyncExternalStore`. Ler
52
+ `window.location` dentro de `useState` ou `useEffect` permite que, na renderização concorrente, dois
53
+ componentes mostrem URLs diferentes no mesmo commit.
44
54
 
45
55
  ## `replace`: quando o passo não é história
46
56
 
@@ -48,14 +58,23 @@ sincronizados pela mesma janela.
48
58
  navigate(`/reports?report=${file}`, { replace: true })
49
59
  ```
50
60
 
51
- Use quando a mudança não merece uma volta do botão "voltar" — um rascunho que ganha id no meio do fluxo, um filtro que a pessoa ajusta várias vezes seguidas. O default (`pushState`) é o certo para navegação de verdade.
61
+ Use `replace` quando a mudança não deve virar um passo do botão Voltar: um rascunho que ganha id no
62
+ meio do fluxo, um filtro que a pessoa ajusta várias vezes seguidas. Para uma navegação entre
63
+ destinos, mantenha o padrão, `pushState`.
52
64
 
53
65
  ## SSR
54
66
 
55
- Os hooks têm snapshot de servidor constante (`/` e busca vazia): renderizam sem `window` e reconciliam com a URL real no primeiro render do cliente. Se a sua página precisa do path correto **no HTML servido**, isto não resolve — aí o path tem que vir do framework, por prop.
67
+ No servidor, os hooks usam um valor fixo (`/` e busca vazia): renderizam sem `window` e passam a
68
+ refletir a URL real no primeiro render do cliente. Se a página precisa do caminho correto no HTML
69
+ servido, receba o caminho do framework por prop.
56
70
 
57
- O `navigate` **não** tem essa cortesia: ele toca `window` direto e estoura no servidor. É de propósito — ler a URL sem browser tem resposta razoável, navegar não tem, e engolir a chamada em silêncio esconderia o erro de uso.
71
+ `navigate` acessa `window` diretamente e lança um erro quando chamado no servidor. Ler a URL fora
72
+ do navegador tem uma resposta razoável; navegar não tem, e ignorar a chamada em silêncio esconderia
73
+ o erro de uso.
58
74
 
59
- ## Destino externo não é problema daqui
75
+ ## Validar destinos vindos de fora
60
76
 
61
- `navigate` espera um caminho da própria origem. Validar destino que veio de terceiro — o `?redirect=` de um pós-login, por exemplo — é responsabilidade de quem recebe esse input, e a checagem certa é resolver com `new URL(valor, origin)` e comparar `origin`, não olhar prefixo.
77
+ `navigate` espera um caminho da própria origem e não valida destinos. Quando o destino vem de fora,
78
+ como o `?redirect=` de um pós-login, valide-o antes de navegar: resolva com
79
+ `new URL(valor, origin)` e compare o `origin` do resultado, em vez de conferir apenas o prefixo do
80
+ texto.
@@ -4,94 +4,130 @@ title: Runtime & adapters
4
4
 
5
5
  # Runtime & adapters
6
6
 
7
- O Opus não é framework — é **protocolo + SDK**: você compõe o runtime a partir de
8
- adapters plugáveis, e cada capacidade é opcional. O litmus é a inversão de controle:
9
- o runtime não toma o seu app; o seu app monta o runtime com as peças que quer.
7
+ O runtime executa as actions registradas e entrega a elas as capacidades que o app configurou:
8
+ servidor HTTP, banco, autenticação, auditoria e as demais. Cada capacidade é um adapter opcional.
9
+ O app monta o runtime com as peças de que precisa, e o Opus não assume o controle do restante da
10
+ aplicação.
10
11
 
11
12
  ## O modelo
12
13
 
13
- > Todo adapter implementa `name`/`kind`; `init`/`dispose`/`healthCheck` são opcionais e
14
- > entram no ciclo de vida do runtime (start inicializa, dispose desliga em ordem reversa,
15
- > healthCheck agrega).
14
+ Todo adapter declara `name` e `kind`. `init`, `dispose` e `healthCheck` são opcionais e acompanham o
15
+ ciclo de vida do runtime: `start()` inicializa os adapters, `dispose()` os desliga em ordem inversa
16
+ e `healthCheck()` reúne o estado de todos.
16
17
 
17
18
  ```ts
18
19
  import { createRuntime } from '@softize/opus/core'
19
20
 
20
21
  const runtime = createRuntime({
21
- server: fastifyServer({ app }), // monta as actions como endpoints
22
+ server: fastifyServer({ app }), // expõe as actions como endpoints
22
23
  data: kyselyData({ db }), // fornece ctx.db
23
- auth: makeAuthAdapter(), // resolve user/tenant/can
24
- audit: [pgAudit({ pool })], // trilha de auditoria (N sinks)
25
- observability: makeObservability(), // span ativo + TraceContext vendor-neutral
24
+ auth: jwtAuth({ secret, mapUser }), // resolve user, tenant e can
25
+ audit: [pgAudit({ pool })], // trilha de auditoria; aceita vários sinks
26
+ observability: openTelemetryObservability(), // span ativo e trace sem fornecedor fixo
26
27
  storage: fsStorage({ root }), // fornece ctx.storage (experimental)
27
28
  config: { env, i18n: { defaultLocale: 'pt-BR' } },
28
29
  })
29
30
 
30
- runtime.register([dealsDomain, billingDomain]) // não registrado = não existe
31
+ runtime.register([workspaceDomain])
31
32
  await runtime.start()
32
33
  ```
33
34
 
35
+ ## Registrar domínios
36
+
37
+ Só o que foi registrado existe para o runtime: uma action fora do registro não é montada no servidor
38
+ nem pode ser executada. `runtime.register` aceita domínios criados com `defineDomain` e também
39
+ actions, reactions e schedules soltos, em uma mesma lista. Um domínio é achatado com seus
40
+ subdomínios; um nome repetido de action, reaction, schedule ou Produto de Dados lança erro, inclusive
41
+ entre domínios registrados separadamente.
42
+
43
+ O `start()` falha quando há reactions registradas sem `eventBus` (`runtime.eventbus_required`) ou
44
+ schedules sem `scheduler` (`runtime.scheduler_required`). A declaração de um domínio está em
45
+ [Actions & contratos](/docs/actions).
46
+
34
47
  ## As capacidades
35
48
 
36
- > Um subpath por driver; quem não usa, não instala (peers opcionais onde há dependência
37
- > pesada). A tabela é o mapa — cada linha é um recurso do protocolo.
49
+ Cada driver fica num subpath próprio do pacote. Drivers que dependem de bibliotecas maiores as
50
+ declaram como dependências opcionais: só quem usa o driver as instala.
38
51
 
39
52
  | Capacidade | Fornece | Driver pronto |
40
53
  |---|---|---|
41
- | `server` | monta as actions (HTTP) | `fastifyServer` — `@softize/opus/server/fastify` · `nodeServer` — `…/server/node` |
42
- | `data` | `ctx.db` + drift-check + CRUD | `kyselyData`, `kyselyRepo`, `crudActions` — `@softize/opus/data/kysely` |
43
- | `auth` | `user`/`tenantId`/`can` do contexto | `jwtAuth` — `…/auth/jwt` · `betterAuthSession` — `…/auth/better-auth` |
54
+ | `server` | expõe as actions por HTTP | `fastifyServer` — `@softize/opus/server/fastify` · `nodeServer` — `…/server/node` |
55
+ | `data` | `ctx.db`, verificação de drift e CRUD | `kyselyData`, `kyselyRepo`, `crudActions` — `@softize/opus/data/kysely` |
56
+ | `auth` | `user`, `tenantId` e `can` do contexto | `jwtAuth` — `…/auth/jwt` · `betterAuthSession` — `…/auth/better-auth` |
44
57
  | `audit` | trilha por execução de action | `pgAudit` — `…/audit/pg` · `consoleAudit` — `…/audit/console` |
45
58
  | `logger` | `ctx.log` estruturado | `pinoLogger` — `@softize/opus/log/pino` |
46
- | `observability` | span ativo + propagação de trace | `openTelemetryObservability` — `@softize/opus/observability/opentelemetry` |
47
- | `eventBus` | `ctx.emit` + **reactions** | `mittEvents` — `@softize/opus/events/mitt` |
59
+ | `observability` | span ativo e propagação de trace | `openTelemetryObservability` — `@softize/opus/observability/opentelemetry` |
60
+ | `eventBus` | `ctx.emit` e reactions | `mittEvents` — `@softize/opus/events/mitt` |
48
61
  | `queue` | jobs em background | `bullmqQueue` — `@softize/opus/queue/bullmq` |
49
- | `scheduler` | **schedules** (cron/intervalo) | `nodeCronScheduler` — `@softize/opus/scheduler/node-cron` |
50
- | `storage` | `ctx.storage` (arquivos) | `fsStorage` — `…/storage/fs` · `s3Storage` — `…/storage/s3` |
51
- | `cache` | `ctx.cache` (leitura, experimental) | `memoryCache` — `@softize/opus/cache/memory` |
52
- | `ai` | `ctx.ai` (complete/extract) | `anthropicAi` — `@softize/opus/ai/anthropic` |
53
- | `client` | chamar actions de fora (stubs) | `fetchClient` — `@softize/opus/client/fetch` |
62
+ | `scheduler` | schedules por cron ou intervalo | `nodeCronScheduler` — `@softize/opus/scheduler/node-cron` |
63
+ | `storage` | `ctx.storage` para arquivos | `fsStorage` — `…/storage/fs` · `s3Storage` — `…/storage/s3` |
64
+ | `cache` | `ctx.cache` de leitura (experimental) | `memoryCache` — `@softize/opus/cache/memory` |
65
+ | `ai` | `ctx.ai`: `complete`, `extract`, `run` e `runStream` | `anthropicAi` — `@softize/opus/ai/anthropic` |
66
+ | `client` | chamar actions de fora do servidor | `fetchClient` — `@softize/opus/client/fetch` |
54
67
 
55
- Fora do runtime, mas parte do protocolo: o harness de teste (`runAction`/`testContext`
56
- — `@softize/opus/testing`) roda uma action em unidade com o mesmo pipeline do
57
- `execute`. Ver **Testes de action**.
68
+ `fastifyServer` devolve o adapter diretamente. `nodeServer` devolve `{ adapter, handler, hasRoute }`:
69
+ passe `adapter` ao runtime e ligue `handler` a um servidor `node:http`, ao Vite ou a um middleware no
70
+ estilo connect.
71
+
72
+ ```ts
73
+ import { createServer } from 'node:http'
74
+ import { nodeServer } from '@softize/opus/server/node'
75
+
76
+ const node = nodeServer()
77
+ const runtime = createRuntime({ server: node.adapter })
78
+ createServer(node.handler).listen(3000)
79
+ ```
80
+
81
+ Fora do runtime, o harness de teste (`runAction` e `testContext`, de `@softize/opus/testing`) executa
82
+ uma action em unidade com as etapas centrais do `execute`: valida a entrada, exige autenticação
83
+ quando a action não é pública, avalia o `authorize`, roda o handler e valida a saída. Ele não roda os
84
+ `loads` (os dados carregados entram pela opção `loaded`) nem grava audit. Veja
85
+ [Testes de action](/docs/testing).
58
86
 
59
87
  ## O contexto do handler
60
88
 
61
- > O `ctx` é a soma do que os adapters fornecem — sem adapter, o campo é `null`/`unknown`
62
- > e o handler decide como degradar.
89
+ O `ctx` recebido pelo handler reúne o que os adapters fornecem. Sem o adapter correspondente, o
90
+ campo chega como `null`, ou como `undefined` no caso de `db`, e o handler decide como seguir.
91
+
92
+ - `user`, `tenantId` e `can`, do adapter `auth`.
93
+ - `db`, do adapter `data`.
94
+ - `log`, do `logger`; sem ele, o runtime usa um logger de console.
95
+ - `emit`, que publica no `eventBus`.
96
+ - `storage`, `cache` e `ai`, dos adapters homônimos.
97
+ - `provenance`, que identifica a origem da execução: HTTP, schedule, reaction, job, agente de IA ou
98
+ integração.
99
+ - `trace`, quando há contexto de trace.
100
+ - `meta`, com dados livres da chamada.
63
101
 
64
- `user`/`tenantId`/`can` (auth) · `db` (data) · `log` (logger) · `emit` (eventBus) ·
65
- `storage` (storage) · `cache` (cache) · `ai` (ai) · `provenance` (quem disparou: http, schedule,
66
- reaction…) · `trace` (quando configurado) · `meta`.
102
+ ## Trace e observabilidade
67
103
 
68
- O core não depende de OpenTelemetry. O `ObservabilityAdapter` envolve actions e reactions
69
- com `runInSpan`; o `TraceContext` efetivo também segue para resultado, audit e eventos. Os
70
- envelopes de job recebem o trace da execução que enfileirou; `executeJob()` cria a execução
71
- filha no worker sem reenfileirar a action.
72
- Falha de instrumentação é lenient e nunca repete o handler.
104
+ O core não depende de OpenTelemetry. O `ObservabilityAdapter` envolve actions e reactions com
105
+ `runInSpan`, e o `TraceContext` efetivo segue para o resultado, o audit e os eventos. Um job recebe
106
+ o trace da execução que o enfileirou, e `executeJob()` cria a execução filha no worker sem
107
+ enfileirar a action de novo. Uma falha de instrumentação é tratada de forma tolerante e nunca
108
+ executa o handler outra vez.
73
109
 
74
- O core não extrai headers HTTP. Os drivers Fastify e Node podem fornecer
75
- `ContextBase.trace` com `traceContext: 'w3c'`; o opt-in é desativado por default. Outros
76
- transportes continuam responsáveis por sua fronteira. Em audit, o contexto novo usa
77
- `traceContext`; o campo `trace` legado continua disponível sem mudança.
110
+ O core não lê headers HTTP. Os drivers Fastify e Node preenchem `ContextBase.trace` com a opção
111
+ `traceContext: 'w3c'`, desativada por padrão; outros transportes continuam responsáveis pela própria
112
+ fronteira. O registro de audit traz dois campos: `traceContext`, com o contexto de trace (`traceId`, `spanId` e flags), e `trace`, com o
113
+ `requestId` e a action de origem (`parentActionId`). Os detalhes estão em [Observabilidade](/docs/observability).
78
114
 
79
- Para actions, o callback observável resolve com `ActionResult`: `resultKind: 'action-result'`
80
- indica que o driver deve inspecionar `result.ok`. Uma Promise resolvida com `ok:false` ainda
81
- representa span de erro. Reactions usam `resultKind: 'void'` e rejeitam em falha.
115
+ Para actions, a execução observada resolve com `ActionResult` e declara `resultKind:
116
+ 'action-result'`: o driver inspeciona `result.ok`, e uma Promise resolvida com `ok: false` ainda
117
+ representa um span de erro. Reactions usam `resultKind: 'void'` e rejeitam em caso de falha.
82
118
 
83
- ## Reactions e schedules
119
+ ## Execução sem requisição
84
120
 
85
- > Os dois jeitos de código rodar SEM request: evento de domínio dispara reaction;
86
- > tempo dispara schedule (que executa uma action, com provenance própria).
121
+ Duas formas de código rodam sem uma requisição: um evento de domínio dispara uma
122
+ [reaction](/docs/events), e o tempo dispara um [schedule](/docs/scheduler), que executa uma action
123
+ com provenance própria. Ambos são registrados junto com as actions, e o manifest projeta os três.
87
124
 
88
- Declarados como as actions e registrados no mesmo `runtime.register` — o manifest
89
- projeta os três. `opus introspect` lista reactions, schedules e o wiring entre eles, mas por
90
- enquanto só reconhece actions declaradas com `defineAction`: as do split `defineContract` +
91
- `bindAction` ficam de fora do modelo que ele monta.
125
+ `opus introspect` lista reactions, schedules e as ligações entre eles, mas só reconhece
126
+ declarações feitas com `defineAction`, `defineReaction` e `defineSchedule`. Actions do par
127
+ `defineContract` e `bindAction` e reactions do par `defineReactionContract` e `bindReaction` ficam de
128
+ fora do modelo que ele monta.
92
129
 
93
- ## Referência profunda
130
+ ## Referência completa
94
131
 
95
- O contrato completo (tipos, garantias, ordem de execução, erros) vive no
96
- `docs/protocol.md` **dentro do pacote** — como o changelog, viaja com a versão
97
- que você instalou.
132
+ O contrato completo, com tipos, garantias, ordem de execução e erros, está em `docs/protocol.md`,
133
+ dentro do pacote instalado. Como o changelog, ele acompanha a versão que o projeto usa.
@@ -4,62 +4,94 @@ title: Agendador
4
4
 
5
5
  # Agendador
6
6
 
7
- Use um schedule para executar uma action por cron ou intervalo. O `SchedulerAdapter` registra o
8
- agendamento e o runtime executa a action com provenance `schedule`, preservando sua rastreabilidade.
7
+ Use um schedule para executar uma action por cron ou em intervalos, sem uma requisição. O
8
+ `SchedulerAdapter` registra o agendamento, e o runtime executa a action com provenance `schedule`,
9
+ que aparece no audit e no contexto do handler.
9
10
 
10
11
  ## O contrato
11
12
 
12
13
  ```ts
13
- interface SchedulerAdapter {
14
+ interface SchedulerAdapter extends Adapter {
15
+ name: string // herdado de Adapter; identifica o driver
16
+ kind: 'scheduler'
14
17
  register(spec: ScheduleDef, fire: () => Promise<void> | void): Unsubscribe
15
18
  }
16
19
 
17
20
  interface ScheduleDef {
18
21
  name: string
19
- action: string // action do registry, executada quando dispara
20
- cron?: string // '0 9 * * *' (9h todo dia)
21
- every?: string // '1h', '30m', '15s' — atalho de intervalo
22
- timezone?: string // IANA, ex.: 'America/Sao_Paulo' (sem ele: hora local do processo)
23
- input?: unknown | (() => unknown | Promise<unknown>) // estático ou dinâmico
24
- enabled?: boolean
22
+ action: string // action registrada, executada a cada disparo
23
+ cron?: string // '0 9 * * *' (todo dia às 9h)
24
+ every?: string // '15s', '5m', '1h', '1d': atalho de intervalo
25
+ timezone?: string // IANA, como 'America/Sao_Paulo'; sem ela, vale a hora local do processo
26
+ input?: unknown | (() => unknown | Promise<unknown>) // fixo ou calculado a cada disparo
27
+ enabled?: boolean // padrão true
28
+ description?: string
29
+ tags?: string[]
25
30
  }
26
31
  ```
27
32
 
28
- O timing é exclusivo: `cron` **ou** `every`. A action referenciada tem que existir no
29
- registry.
33
+ Declare `cron` ou `every`, não os dois. `every` aceita segundos e minutos até 59, horas até 23 e
34
+ somente `1d` para dias; intervalos maiores usam `cron`. Sem `input`, a action recebe `{}`.
30
35
 
31
36
  ## Driver
32
37
 
33
38
  ```ts
34
39
  import { nodeCronScheduler } from '@softize/opus/scheduler/node-cron'
35
40
 
36
- const scheduler = nodeCronScheduler() // in-process
41
+ const scheduler = nodeCronScheduler() // no mesmo processo do runtime
37
42
  ```
38
43
 
39
- ## No runtime
44
+ ## Declarar no domínio
45
+
46
+ Declare o schedule com `defineSchedule` e registre-o no domínio, ao lado da action que ele executa:
40
47
 
41
48
  ```ts
42
- import { defineSchedule } from '@softize/opus/core'
49
+ import { createRuntime, defineDomain, defineSchedule } from '@softize/opus/core'
50
+ import { caixaFechar } from './caixa-fechar.ts' // a action 'caixa.fechar', definida com bindAction
43
51
 
44
- // Declarado no domínio, junto das actions:
45
52
  const fecharDia = defineSchedule({
46
- name: 'fechar-dia',
53
+ name: 'caixa.fechar-dia',
47
54
  action: 'caixa.fechar',
48
55
  cron: '0 23 * * *',
49
56
  timezone: 'America/Sao_Paulo',
50
57
  })
51
58
 
59
+ export const caixaDomain = defineDomain({
60
+ name: 'caixa',
61
+ actions: { caixaFechar },
62
+ schedules: [fecharDia],
63
+ })
64
+
52
65
  const runtime = createRuntime({
53
66
  // …server/data…
54
67
  scheduler,
55
68
  })
69
+ runtime.register([caixaDomain])
70
+ await runtime.start()
56
71
  ```
57
72
 
58
- O runtime registra os schedules do domínio no `start()` e chama a action quando o tempo bate
59
- — nada de disparar no handler.
73
+ No `start()`, o runtime confere se cada schedule aponta para uma action registrada no mesmo runtime
74
+ e depois o registra no adapter. Um nome sem action registrada interrompe o `start()` com
75
+ `runtime.schedule_action_not_found`, antes de inicializar os adapters; a mensagem lista cada
76
+ schedule e a action que ele procurou. Com schedules registrados e sem `scheduler`, o `start()`
77
+ falha com `runtime.scheduler_required`. Um schedule com `enabled: false` não é registrado nem
78
+ conferido, então pode apontar para uma action que ainda não existe no ambiente.
79
+
80
+ ## Preparar a action agendada
81
+
82
+ O schedule executa a action sem usuário: `ctx.user` é `null` e `ctx.can` nega tudo. Por isso, a
83
+ action alvo precisa declarar `public: true`; sem isso, cada disparo termina em
84
+ `auth.unauthenticated`. Para que ela não fique acessível por HTTP, declare também `internal: true`:
85
+ os servidores do Opus não expõem actions internas, e o schedule continua executando-a pelo runtime.
86
+
87
+ Quando um disparo não conclui a action, o runtime registra um erro no log com o schedule, a action,
88
+ o código e a mensagem do erro; o agendamento continua ativo e o próximo disparo executa a action de
89
+ novo. Uma falha dentro do pipeline da action, como `auth.unauthenticated` ou um erro do handler,
90
+ também fica registrada no audit. O callback `onError` do driver recebe somente erros lançados antes
91
+ da execução, como uma falha na função `input`.
60
92
 
61
93
  ## Limites atuais
62
94
 
63
- Driver hoje: `node-cron` (in-process — some se o processo cair; em um cluster, cada nó
64
- dispararia). Backends duráveis/distribuídos (BullMQ repeatable, Temporal…) entram por
65
- reincidência.
95
+ O único driver é o `node-cron`, que roda no mesmo processo: o agendamento some se o processo cair e,
96
+ em um cluster, cada nó dispara o mesmo schedule. O modo design do Vite não registra schedules.
97
+ Backends duráveis ou distribuídos, como jobs repetidos do BullMQ ou Temporal, ainda não têm driver.