@softize/opus 19.0.0 → 20.1.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 (148) hide show
  1. package/CHANGELOG.md +239 -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/0020-icon-tiles-are-explicitly-composed.md +8 -0
  7. package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
  9. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  10. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
  11. package/docs/data-products.md +15 -9
  12. package/docs/protocol.md +3 -4
  13. package/docs/releasing.md +2 -2
  14. package/package.json +2 -2
  15. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  16. package/registry/templates/app/package.json +1 -1
  17. package/src/core/runtime.ts +96 -8
  18. package/src/core/types.ts +5 -2
  19. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  20. package/src/ui/components/patterns/content-header.tsx +109 -18
  21. package/src/ui/components/patterns/fill-region.ts +150 -0
  22. package/src/ui/components/patterns/list.tsx +91 -92
  23. package/src/ui/components/patterns/page.tsx +20 -4
  24. package/src/ui/components/patterns/presentation.tsx +52 -37
  25. package/src/ui/components/patterns/split.tsx +44 -4
  26. package/src/ui/components/patterns/surface-anatomy.ts +35 -8
  27. package/src/ui/components/patterns/surface-header.tsx +58 -25
  28. package/src/ui/components/patterns/trigger.tsx +12 -2
  29. package/src/ui/components/primitives/alert.tsx +9 -1
  30. package/src/ui/components/primitives/avatar.tsx +7 -3
  31. package/src/ui/components/primitives/card.tsx +1 -1
  32. package/src/ui/components/primitives/carousel.tsx +34 -5
  33. package/src/ui/components/primitives/checkbox.tsx +11 -3
  34. package/src/ui/components/primitives/command.tsx +2 -1
  35. package/src/ui/components/primitives/copyable.tsx +120 -10
  36. package/src/ui/components/primitives/dialog.tsx +42 -11
  37. package/src/ui/components/primitives/drawer.tsx +10 -2
  38. package/src/ui/components/primitives/empty.tsx +1 -1
  39. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  40. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  41. package/src/ui/components/primitives/input-group.tsx +9 -2
  42. package/src/ui/components/primitives/input-otp.tsx +1 -1
  43. package/src/ui/components/primitives/kbd.tsx +1 -1
  44. package/src/ui/components/primitives/label.tsx +3 -0
  45. package/src/ui/components/primitives/menu.tsx +5 -0
  46. package/src/ui/components/primitives/popover.tsx +1 -1
  47. package/src/ui/components/primitives/progress.tsx +9 -1
  48. package/src/ui/components/primitives/slider.tsx +62 -11
  49. package/src/ui/components/primitives/truncate.tsx +10 -1
  50. package/src/ui/docs/content/accordion.md +74 -27
  51. package/src/ui/docs/content/action-form-card.md +24 -15
  52. package/src/ui/docs/content/action-form-dialog.md +17 -7
  53. package/src/ui/docs/content/action-form.md +92 -77
  54. package/src/ui/docs/content/action-list-dialog.md +75 -38
  55. package/src/ui/docs/content/action-list.md +141 -58
  56. package/src/ui/docs/content/action-trigger.md +27 -10
  57. package/src/ui/docs/content/action-view.md +14 -14
  58. package/src/ui/docs/content/actions.md +161 -66
  59. package/src/ui/docs/content/ai.md +43 -44
  60. package/src/ui/docs/content/alert.md +83 -29
  61. package/src/ui/docs/content/ask.md +51 -26
  62. package/src/ui/docs/content/aspect-ratio.md +22 -24
  63. package/src/ui/docs/content/audit.md +23 -19
  64. package/src/ui/docs/content/auth.md +38 -19
  65. package/src/ui/docs/content/avatar.md +54 -5
  66. package/src/ui/docs/content/badge.md +29 -9
  67. package/src/ui/docs/content/breadcrumb.md +21 -8
  68. package/src/ui/docs/content/button.md +73 -43
  69. package/src/ui/docs/content/calendar.md +134 -26
  70. package/src/ui/docs/content/card.md +41 -15
  71. package/src/ui/docs/content/carousel.md +37 -6
  72. package/src/ui/docs/content/chat.md +118 -37
  73. package/src/ui/docs/content/checkbox.md +73 -7
  74. package/src/ui/docs/content/cli.md +94 -55
  75. package/src/ui/docs/content/collapsible.md +43 -19
  76. package/src/ui/docs/content/command.md +30 -8
  77. package/src/ui/docs/content/composer.md +23 -8
  78. package/src/ui/docs/content/content.md +119 -27
  79. package/src/ui/docs/content/copyable.md +39 -8
  80. package/src/ui/docs/content/customization.md +55 -46
  81. package/src/ui/docs/content/cycle.md +33 -18
  82. package/src/ui/docs/content/data-products.md +115 -0
  83. package/src/ui/docs/content/data-state.md +20 -6
  84. package/src/ui/docs/content/data.md +20 -19
  85. package/src/ui/docs/content/detail.md +12 -5
  86. package/src/ui/docs/content/dialog.md +107 -24
  87. package/src/ui/docs/content/dictionary-value.md +4 -4
  88. package/src/ui/docs/content/dock.md +54 -19
  89. package/src/ui/docs/content/dot.md +35 -5
  90. package/src/ui/docs/content/drawer.md +63 -19
  91. package/src/ui/docs/content/empty-value.md +20 -6
  92. package/src/ui/docs/content/empty.md +24 -3
  93. package/src/ui/docs/content/events.md +76 -16
  94. package/src/ui/docs/content/executions.md +9 -9
  95. package/src/ui/docs/content/field.md +21 -6
  96. package/src/ui/docs/content/getting-started.md +44 -34
  97. package/src/ui/docs/content/icon-picker.md +19 -13
  98. package/src/ui/docs/content/icon-tile.md +38 -15
  99. package/src/ui/docs/content/input-otp.md +58 -7
  100. package/src/ui/docs/content/input.md +22 -3
  101. package/src/ui/docs/content/item.md +53 -6
  102. package/src/ui/docs/content/kbd.md +5 -1
  103. package/src/ui/docs/content/label.md +13 -4
  104. package/src/ui/docs/content/log.md +5 -5
  105. package/src/ui/docs/content/markdown.md +5 -4
  106. package/src/ui/docs/content/mcp.md +49 -23
  107. package/src/ui/docs/content/menu.md +95 -10
  108. package/src/ui/docs/content/metric-card.md +12 -4
  109. package/src/ui/docs/content/observability.md +10 -9
  110. package/src/ui/docs/content/page.md +118 -12
  111. package/src/ui/docs/content/pagination.md +13 -5
  112. package/src/ui/docs/content/pane.md +55 -5
  113. package/src/ui/docs/content/popover.md +42 -5
  114. package/src/ui/docs/content/presentation.md +91 -47
  115. package/src/ui/docs/content/progress.md +27 -14
  116. package/src/ui/docs/content/queue.md +39 -21
  117. package/src/ui/docs/content/radio-group.md +26 -4
  118. package/src/ui/docs/content/router.md +29 -10
  119. package/src/ui/docs/content/runtime.md +90 -54
  120. package/src/ui/docs/content/scheduler.md +53 -21
  121. package/src/ui/docs/content/scroll-area.md +42 -35
  122. package/src/ui/docs/content/select.md +78 -33
  123. package/src/ui/docs/content/semantic-context.md +21 -2
  124. package/src/ui/docs/content/separator.md +15 -9
  125. package/src/ui/docs/content/sidebar.md +3 -2
  126. package/src/ui/docs/content/skeleton.md +6 -1
  127. package/src/ui/docs/content/slider.md +37 -9
  128. package/src/ui/docs/content/spinner.md +4 -3
  129. package/src/ui/docs/content/split.md +19 -0
  130. package/src/ui/docs/content/storage.md +8 -9
  131. package/src/ui/docs/content/switch.md +10 -7
  132. package/src/ui/docs/content/table.md +46 -8
  133. package/src/ui/docs/content/tabs.md +14 -4
  134. package/src/ui/docs/content/testing.md +76 -31
  135. package/src/ui/docs/content/textarea.md +5 -4
  136. package/src/ui/docs/content/toast.md +48 -7
  137. package/src/ui/docs/content/toggle.md +3 -3
  138. package/src/ui/docs/content/tokens.md +15 -26
  139. package/src/ui/docs/content/tooltip.md +18 -2
  140. package/src/ui/docs/content/truncate.md +36 -6
  141. package/src/ui/docs/content/ui.md +74 -22
  142. package/src/ui/docs/content/upgrading.md +45 -36
  143. package/src/ui/docs/plugin.ts +36 -2
  144. package/src/ui/docs/registry.tsx +6 -0
  145. package/src/ui/drivers/react.tsx +16 -8
  146. package/src/ui/meta.ts +32 -32
  147. package/src/ui/react.tsx +1 -0
  148. package/src/vite/design.ts +16 -4
@@ -5,18 +5,19 @@ title: Actions & contratos
5
5
  # Actions & contratos
6
6
 
7
7
  Uma action descreve uma operação que cliente e servidor precisam compreender da mesma forma.
8
- Seu contrato reúne entidade, entrada, saída e metadados no package compartilhado. A API conecta
9
- o handler e a web usa a mesma definição para executar ou renderizar a operação.
8
+ Seu contrato reúne entrada, saída, documentação e regras de acesso num módulo compartilhado. O
9
+ servidor liga o handler a esse contrato, e a interface usa a mesma definição para executar ou
10
+ renderizar a operação.
10
11
 
11
12
  ## Contrato primeiro
12
13
 
13
- > `defineContract` no `shared/` — entidades, schemas (zod + `t.*` para tipos lógicos) e os
14
- > metadados da action. `api` e `web` importam; nada se duplica.
14
+ O contrato fica no código que roda nos dois lados. Ele declara identidade, schemas (zod e os tipos
15
+ lógicos `t.*`) e metadados, sem depender de banco, segredo ou driver de servidor. `api` e `web`
16
+ importam o mesmo objeto.
15
17
 
16
18
  ```ts
17
- // shared/ — roda nos dois lados.
18
- import { defineContract } from '@softize/opus'
19
- import { t } from '@softize/opus/schema/zod'
19
+ // shared/ — roda no cliente e no servidor.
20
+ import { defineContract } from '@softize/opus/core'
20
21
  import { z } from 'zod'
21
22
 
22
23
  export const workspaceListContract = defineContract({
@@ -25,21 +26,24 @@ export const workspaceListContract = defineContract({
25
26
  summary: 'Lista os workspaces do back-office',
26
27
  description: 'O workspace é a entidade raiz que agrupa repositórios, apps e agentes…',
27
28
  input: z.object({ q: z.string().optional() }),
28
- output: workspaceRowSchema,
29
+ output: workspaceRowSchema, // em `list`, o schema de cada item
29
30
  paginate: 'cursor',
30
31
  })
31
32
  ```
32
33
 
33
- ## A action
34
+ O nome segue `<recurso>.<verbo>` e o `kind` é `simple`, `form`, `list` ou `view`. Cada action fica
35
+ em um arquivo próprio, em `src/domains/<recurso>/actions/`.
34
36
 
35
- > `name = <resource>.<verb>`; `kind ∈ simple | form | list | view`; uma por arquivo em
36
- > `src/domains/<resource>/actions/`, registrada no runtime. Não registrada = não existe.
37
+ ## Ligar o handler no servidor
38
+
39
+ `bindAction` junta ao contrato a parte que só existe no servidor e produz a action executável. O
40
+ contrato continua importável pela web sem arrastar o acesso ao banco.
37
41
 
38
42
  ```ts
39
- // api/ — server-only. bindAction amarra o handler no contrato compartilhado.
43
+ // api/ — somente no servidor.
40
44
  import { randomUUID } from 'node:crypto'
41
45
  import { bindAction } from '@softize/opus/core'
42
- import { workspaceCreateContract } from '@app/shared' // contrato `form`, declarado no shared/ como o acima
46
+ import { workspaceCreateContract } from '@app/shared' // contrato `form`, declarado como o acima
43
47
 
44
48
  export const workspaceCreate = bindAction(workspaceCreateContract, {
45
49
  handler: async (ctx, input) => {
@@ -50,42 +54,120 @@ export const workspaceCreate = bindAction(workspaceCreateContract, {
50
54
  })
51
55
  ```
52
56
 
57
+ O binding aceita `handler`, `loads`, `authorize`, `background`, `emits` e `idempotency`. Exceto
58
+ `authorize`, esses campos não existem no contrato: `defineContract` recusa qualquer um deles pelo
59
+ tipo. O `authorize` pode aparecer nos dois lados; o declarado no binding substitui o do contrato. O handler recebe a entrada já validada pelo schema, com os
60
+ valores padrão aplicados.
61
+
62
+ [`background`](/docs/queue) e [`emits`](/docs/events) só têm efeito em actions `simple` e `form`.
63
+ O tipo do binding os aceita em `list` e `view`, mas ali o runtime os ignora: a action executa na
64
+ própria requisição e a lista de eventos não entra no manifest. Fora de produção, o runtime avisa ao
65
+ registrar a action (`runtime.action_field_ignored`), e o [`opus check`](/docs/cli) aponta a regra
66
+ `ignored-by-kind`.
67
+
68
+ ## Carregar dados antes de autorizar
69
+
70
+ Quando a permissão depende do registro, como “só quem criou a tarefa pode editá-la”, declare em
71
+ `loads` o que precisa ser buscado. O runtime carrega esses dados antes do `authorize` e os entrega
72
+ também ao handler.
73
+
74
+ ```ts
75
+ export const taskUpdate = bindAction(taskUpdateContract, {
76
+ loads: { task: 'task(:id)' }, // busca a tarefa pelo `id` do input
77
+ authorize: 'task.ownerId == user.id', // expressão avaliada com user, input e o que foi carregado
78
+ handler: async (ctx, input, loaded) => {
79
+ // `loaded.task` já passou pela autorização.
80
+ },
81
+ })
82
+
83
+ // Na montagem do runtime: como resolver cada entidade citada em `loads`.
84
+ runtime.registerLoaders({
85
+ task: (args, ctx) => findTask(ctx.db, String(args.id)),
86
+ })
87
+ ```
88
+
89
+ Uma entidade sem resolver registrado falha com `runtime.loader_not_registered`. Quando o resolver
90
+ devolve `null` ou `undefined`, a action responde `<action>.<chave>.not_found`. `loads` também aceita
91
+ uma função `(ctx, input) => Promise<unknown>` no lugar da expressão. A ordem completa das
92
+ verificações está em [Autenticação](/docs/auth).
93
+
94
+ ## Registrar em um domínio
95
+
96
+ Uma action só existe para o runtime depois de registrada. `defineDomain` agrupa as actions,
97
+ reactions, schedules, entidades e [Produtos de Dados](/docs/data-products) de um mesmo recorte
98
+ funcional:
99
+
100
+ ```ts
101
+ // src/domains/workspace/index.ts
102
+ import { defineDomain } from '@softize/opus/core'
103
+ import { workspaceCreate } from './actions/create.ts'
104
+ import { workspaceList } from './actions/list.ts'
105
+
106
+ export const workspaceDomain = defineDomain({
107
+ name: 'workspace',
108
+ description: 'Workspaces do back-office e o que eles agrupam.',
109
+ actions: { workspaceCreate, workspaceList },
110
+ })
111
+ ```
112
+
113
+ Registre o domínio no runtime com `runtime.register([workspaceDomain])` e liste-o em `domains` no
114
+ `opus.config.ts`, que é de onde `opus gen` projeta o manifest.
115
+
116
+ `defineDomain` valida a declaração no momento em que roda: o `name` segue `[a-z][a-zA-Z0-9_]*`,
117
+ duas actions do domínio (subdomínios incluídos) não podem ter o mesmo nome e cada Produto de Dados
118
+ precisa citar actions e entidades existentes. Ao registrar, o runtime avisa no console quando o
119
+ nome de uma action não começa pelo caminho do domínio, como `workspace.` ou `workspace.member.`.
120
+ `subdomains` aceita outros domínios e forma esse caminho.
121
+
122
+ `actions` aceita um mapa, uma lista ou o resultado de `import * as actions`. Um domínio também pode
123
+ registrar só o contrato, sem handler: no modo design, descrito abaixo, o `mockHandler` responde;
124
+ fora dele, a execução falha por falta de handler.
125
+
53
126
  ## Ordem canônica dos campos
54
127
 
55
- > Esta ordem mantém contratos previsíveis para leitura e geração. O `opus check` informa os
56
- > campos que precisam ser reposicionados, e a skill `create-opus-action` já cria o contrato e o
57
- > binding nessa estrutura.
128
+ Manter os campos na mesma ordem torna contratos previsíveis para leitura e geração. O `opus check`
129
+ aponta o primeiro campo fora de ordem, e a skill `$create-opus-action` já cria contrato e binding
130
+ nessa estrutura. Os grupos seguem esta sequência; dentro de cada grupo, a ordem é livre:
58
131
 
59
132
  ```text
60
- name → kind → (label / summary / messages / tags…)
61
- → input → output
62
- → (authorize)
63
- → fields / paginate
64
- → handler
65
- → invalidates
133
+ name, kind
134
+ → label, title, summary, description, icon, color, messages, tags, examples, errors
135
+ → input, output
136
+ → public, requires, authorize, loads
137
+ → handler, mockHandler
138
+ → audit, confirm, invalidates, rateLimit, idempotency
139
+ → ai, automatable, internal
66
140
  ```
67
141
 
68
- Fail-closed por padrão: `input`/`output` via `z.object` + `t.*` para os tipos lógicos; `authorize`
69
- declarado ou gate explícito do adapter.
142
+ Campos específicos de um kind, como `fields`, `paginate`, `columns`, `filters`, `expand`,
143
+ `background` e `emits`, ficam fora dessa verificação.
70
144
 
71
- Os átomos do catálogo servem também dentro dos schemas de input/output — não re-escreva
72
- regex do que o Opus já valida: `z.object({ tag: t.slug().zod(), contato: t.email().zod() })`.
73
- O `.zod()` devolve o schema zod subjacente do tipo lógico (slug, email, phone, money…).
145
+ Declare `input` e `output` com `z.object` e use os tipos lógicos do catálogo em vez de reescrever
146
+ validações: `z.object({ tag: t.slug().zod(), contato: t.email().zod() })`. O `.zod()` devolve o
147
+ schema zod de cada tipo lógico (slug, email, phone, money…), importado de `@softize/opus/schema/zod`.
74
148
 
75
- ## Modo design — o mock mora no contrato
149
+ A autorização é fechada por padrão: sem `public: true`, a action exige usuário autenticado; com
150
+ `authorize`, exige também a permissão declarada.
76
151
 
77
- Um `mockHandler` no contrato deixa a UI rodar com dado realista **sem backend nem banco**: em
78
- `OPUS_MODE=design` o runtime roteia o `execute` para o `mockHandler` (cai no `handler` real se
79
- ausente). Como ele fica no contrato, e não no `bindAction`, é isomorfo — o design autora, o
80
- handoff pluga o handler, e o mock nem toca `ctx.db`.
152
+ ## Prototipar com dados simulados
81
153
 
82
- Alimente com `fake`/`fakeMany` (fixtures determinísticas do próprio schema — ver [Testes](testing)):
154
+ Para desenhar a interface antes de o backend existir, declare um `mockHandler` no contrato. Com
155
+ `OPUS_MODE=design`, o runtime executa o `mockHandler` no lugar do `handler`; sem ele, usa o
156
+ `handler` real. Como o mock fica no contrato, a mesma declaração serve ao design e, depois, ao
157
+ handler real, e o mock não precisa acessar `ctx.db`.
158
+
159
+ `fake` e `fakeMany` geram dados determinísticos a partir do próprio schema (ver
160
+ [Testes de action](/docs/testing)). Em `list`, o `mockHandler` devolve a página inteira, com
161
+ `items`, `cursor` e `total`:
83
162
 
84
163
  ```ts
164
+ import { bindAction, defineContract } from '@softize/opus/core'
85
165
  import { entityRowSchema } from '@softize/opus/schema'
86
166
  import { fake, fakeMany } from '@softize/opus/testing'
167
+ import { z } from 'zod'
168
+ import { EventEntity } from '../entity.ts'
87
169
 
88
- // `defineEntity` devolve o próprio config; o schema da linha vem de `entityRowSchema`.
170
+ // `entityRowSchema` deriva da entidade o schema de uma linha.
89
171
  const eventRowSchema = entityRowSchema(EventEntity)
90
172
 
91
173
  export const eventGet = defineContract({
@@ -93,19 +175,30 @@ export const eventGet = defineContract({
93
175
  kind: 'view',
94
176
  input: z.object({ id: z.string().uuid() }),
95
177
  output: eventRowSchema,
96
- mockHandler: () => fake(eventRowSchema), // 1 evento fake
178
+ mockHandler: () => fake(eventRowSchema),
97
179
  })
98
- // listas: `mockHandler: () => fakeMany(eventRowSchema, 20)`
99
180
 
100
- // o handoff, depois, só pluga o real — mesmo contrato:
101
- export const eventGetImpl = bindAction(eventGet, { handler: async (ctx, input) => { /* db */ } })
181
+ export const eventList = defineContract({
182
+ name: 'event.list',
183
+ kind: 'list',
184
+ input: z.object({}),
185
+ output: eventRowSchema,
186
+ paginate: 'cursor',
187
+ mockHandler: async () => ({ items: fakeMany(eventRowSchema, 20), cursor: { next: null }, total: 20 }),
188
+ })
189
+
190
+ // Depois, o servidor liga o handler real ao mesmo contrato.
191
+ export const eventGetImpl = bindAction(eventGet, {
192
+ handler: async (ctx, input) =>
193
+ ctx.db.selectFrom('events').selectAll().where('id', '=', input.id).executeTakeFirstOrThrow(),
194
+ })
102
195
  ```
103
196
 
104
- O `mockHandler` roda no **servidor em modo design** (`OPUS_MODE=design`), não no cliente — e
105
- quem sobe esse servidor é o próprio dev server: o plugin `opusDesign()` (`@softize/opus/vite`)
106
- monta o runtime Opus DENTRO do vite e serve `/api` in-process, com os mocks respondendo e
107
- qualquer `server.proxy` para backend externo desligado (prefixo ex-proxy sem cobertura responde
108
- 503 em envelope — nada vaza para prod). A SPA chama `/api` normal e recebe o dado fake, isolado:
197
+ O `mockHandler` roda no servidor, não no navegador. O plugin `opusDesign()`, de
198
+ `@softize/opus/vite`, monta o runtime do Opus dentro do dev server e responde `/api` no próprio
199
+ processo. No modo design, qualquer `server.proxy` para um backend externo fica desligado: um prefixo
200
+ que ia para o proxy e não tem action correspondente responde 503 em envelope, sem alcançar o
201
+ backend. A aplicação chama `/api` como sempre e recebe os dados simulados.
109
202
 
110
203
  ```ts
111
204
  // vite.config.ts — o plugin só carrega no modo design, por import dinâmico.
@@ -121,31 +214,32 @@ async function designPlugin(): Promise<PluginOption> {
121
214
  const designPlugins: PluginOption[] = DESIGN_RUN ? [await designPlugin()] : []
122
215
 
123
216
  export default defineConfig({ plugins: [react(), ...designPlugins] })
124
- // sobe com: vite --mode design --configLoader runner (script dev:design do template)
217
+ // Execute com: vite --mode design --configLoader runner (script dev:design do template)
125
218
  ```
126
219
 
127
- O import fica dinâmico e condicionado porque o Opus publica TypeScript: o carregador padrão de
128
- config do Vite externaliza um import estático, e o Node 22+ recusa type stripping dentro de
129
- `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). Com `--configLoader runner`, o Vite
130
- transforma o TypeScript antes de avaliar o plugin. O app criado por `opus create` já traz esse arranjo.
220
+ O import é dinâmico e condicionado porque o Opus publica TypeScript: o carregador padrão de config
221
+ do Vite externaliza um import estático, e o Node 22+ recusa type stripping dentro de `node_modules`
222
+ (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). Com `--configLoader runner`, o Vite transforma o
223
+ TypeScript antes de avaliar o plugin. O app criado por `opus create` já traz esse arranjo.
224
+
225
+ O runtime de design atende somente requisições: reactions e schedules dos módulos carregados não
226
+ são registrados.
131
227
 
132
- O `entry` (default `./opus.config.ts`) também aceita **glob** — `opusDesign({ entry:
133
- 'src/domains/*/contract.ts' })` — re-expandido a cada rebuild: **domínio novo entra no ar ao
134
- salvar o arquivo**, sem editar entry nem reiniciar o dev server (criar o arquivo já invalida o
135
- runtime; o rebuild é lazy, na request seguinte). E o 404 de action desconhecida **se cura
136
- sozinho**: antes de responder, o plugin força um rebuild (um por ciclo de mudança) e re-tenta —
137
- persistindo, o envelope traz a mensagem-guia apontando o entry.
228
+ O `entry` do plugin (padrão `./opus.config.ts`) também aceita glob, como
229
+ `opusDesign({ entry: 'src/domains/*/contract.ts' })`. O glob é expandido de novo a cada
230
+ reconstrução: um domínio novo passa a responder assim que o arquivo é salvo, sem editar o `entry`
231
+ nem reiniciar o dev server. Quando uma action não é encontrada, o plugin reconstrói o runtime uma
232
+ vez por ciclo de mudança e tenta de novo; se a action continuar ausente, a resposta indica o
233
+ `entry` a conferir.
138
234
 
139
- Como o contrato é isomorfo, o `mockHandler` também acompanha o **bundle web** — peso morto lá
140
- (a SPA nunca o chama; o dado é fake). Tirá-lo do bundle de prod é um transform de build
141
- (deferido); **não** dá para gatear no call-site com `import.meta.env` sem quebrar o servidor
142
- (não existe em Node). É a materialização Opus-nativa do "mock = contrato": o design não é
143
- descartável — vira o contrato que o backend honra.
235
+ Como o contrato vai para os dois lados, o `mockHandler` também entra no bundle da web, onde nunca é
236
+ chamado. Não o condicione com `import.meta.env` no contrato: essa variável não existe no Node e
237
+ quebraria o servidor.
144
238
 
145
- ## O front consome o contrato
239
+ ## Consumir o contrato na interface
146
240
 
147
- > Nunca fetch ou tipos na mão. Os hooks e os patterns (ActionForm/ActionList…) renderizam a
148
- > action inteira a partir do contrato — o spec é a fonte, o pattern é o renderizador.
241
+ A interface não precisa de fetch nem de tipos escritos à mão. Os hooks e os componentes de action,
242
+ como `ActionForm` e `ActionList`, executam e renderizam a operação a partir do contrato:
149
243
 
150
244
  ```tsx
151
245
  import { ActionForm, useListAction } from '@softize/opus/ui/react'
@@ -153,10 +247,11 @@ import { ActionForm, useListAction } from '@softize/opus/ui/react'
153
247
  // Lista declarativa: busca ao montar e refaz quando uma action invalida `workspace.list`.
154
248
  const { items, total, isLoading } = useListAction<WorkspaceRow>(workspaceListContract, { q })
155
249
 
156
- // Form contract-driven: os campos (fields) moram NO contrato.
250
+ // Formulário a partir do contrato: os campos vêm de `fields`, declarado no próprio contrato.
157
251
  <ActionForm action={workspaceCreateContract} onSuccess={…} />
158
252
  ```
159
253
 
160
- `useListAction` exige `OpusProvider` e um `QueryClientProvider` acima da árvore. Para buscar sob demanda, como num
161
- autocomplete, use `useLookupAction`: ele não busca ao montar, expõe `run(input)` e `reset()` e
162
- devolve `items`, `cursor`, `total` e os estados da última execução.
254
+ Os hooks precisam de `OpusProvider` e `QueryClientProvider` acima da árvore; a montagem está em
255
+ [Como consumir](/ui/como-consumir). Para buscar sob demanda, como num autocomplete, use
256
+ `useLookupAction`: ele não busca ao montar, expõe `run(input)` e `reset()` e devolve `items`,
257
+ `cursor`, `total` e os estados da última execução.
@@ -4,14 +4,12 @@ title: IA generativa
4
4
 
5
5
  # IA generativa
6
6
 
7
- > **Experimental.** Superfície mínima — cresce por reincidência de caso real, não por
8
- > especulação (memória longa de conversa entra quando um caso real cobrar).
7
+ > **Experimental.** A superfície é mínima e cresce conforme casos reais a exijam.
9
8
 
10
- Três coisas: **completar** texto, **extrair** dado estruturado, e **rodar um agente** sobre
11
- as actions do app. O contrato é um adapter do core (`AiAdapter`). O diferencial do `extract`
12
- é que **o mesmo `Schema` das actions vira o contrato da resposta do modelo** — saída forçada
13
- e validada pelo schema. O do `run` é que **as actions viram as tools** — o modelo age no app,
14
- como o usuário, pelas mesmas actions que a UI usa.
9
+ Use o adapter de IA para completar texto, extrair dados estruturados ou executar um agente sobre as
10
+ actions do app. Em `extract`, o mesmo `Schema` usado nas actions define a resposta do modelo: a saída
11
+ é forçada e validada por ele. Em `run`, as actions expostas viram as ferramentas do modelo, que age
12
+ no app como o usuário, pelas mesmas actions usadas pela interface.
15
13
 
16
14
  ## O contrato
17
15
 
@@ -19,7 +17,7 @@ como o usuário, pelas mesmas actions que a UI usa.
19
17
  interface AiAdapter {
20
18
  complete(prompt: string, opts?: AiCompleteOptions): Promise<string>
21
19
  extract<T>(prompt: string, schema: Schema<T>, opts?: AiCompleteOptions): Promise<T>
22
- // Loop agêntico: o modelo chama tools (as actions ai:enabled) até responder em texto.
20
+ // Loop agêntico: o modelo chama tools (as actions expostas à IA) até responder em texto.
23
21
  run?(input: string | AiMessage[], opts: AiRunOptions): Promise<AiRunResult>
24
22
  // O mesmo loop emitindo ChatEvent conforme acontece (ver Streaming, abaixo).
25
23
  runStream?(input: string | AiMessage[], opts: AiRunOptions): AsyncIterable<ChatEvent>
@@ -32,16 +30,15 @@ interface AiAdapter {
32
30
  ```ts
33
31
  import { anthropicAi } from '@softize/opus/ai/anthropic'
34
32
 
35
- // Default: haiku (rápido/barato) e ANTHROPIC_API_KEY do ambiente.
33
+ // Padrão: modelo Haiku e ANTHROPIC_API_KEY do ambiente.
36
34
  const ai = anthropicAi()
37
35
 
38
36
  // Tudo injetável: client próprio, modelo default, teto de tokens.
39
37
  const custom = anthropicAi({ apiKey, model: 'claude-sonnet-5', maxTokens: 2048 })
40
38
  ```
41
39
 
42
- O peer é **opcional** (`@anthropic-ai/sdk`): quem não usa o driver não o instala —
43
- importado sob demanda. Em teste, injete um client fake (`{ messages: { create } }`)
44
- e nada toca a rede.
40
+ `@anthropic-ai/sdk` é dependência opcional, carregada só quando o driver é usado. Em teste, injete um
41
+ client falso (`{ messages: { create } }`) para que nada acesse a rede.
45
42
 
46
43
  ## No runtime
47
44
 
@@ -54,50 +51,52 @@ const runtime = createRuntime({
54
51
  // No handler: ctx.ai (null quando não configurado).
55
52
  handler: async (ctx, input) => {
56
53
  const meta = await ctx.ai!.extract(
57
- `Classifique o chamado e proponha um título curto: ${input.texto}`,
58
- z.object({ titulo: z.string(), prioridade: z.enum(['baixa', 'media', 'alta']) }),
54
+ `Classifique o chamado e proponha um título curto: ${input.text}`,
55
+ z.object({ title: z.string(), priority: z.enum(['low', 'medium', 'high']) }),
59
56
  )
60
- return meta // já validado pelo schema — fora do contrato, o extract estoura.
57
+ return meta // já validado; uma resposta fora do schema faz o extract lançar erro
61
58
  }
62
59
  ```
63
60
 
64
- ## Actions como tools (o agente)
61
+ ## Actions como ferramentas do agente
65
62
 
66
- Marque uma action com `ai: { enabled: true }` no contrato e ela vira uma **tool** que o
67
- modelo pode chamar. Aí `ctx.ai.run(prompt)` roda o loop agêntico sobre as actions `ai:enabled`:
68
- o modelo escolhe a tool → o runtime executa a action **como o usuário logado** (limitado pelo
69
- `ctx.can`) → o resultado volta para o modelo → repete até a resposta em texto.
63
+ Exponha uma action à IA no contrato, com `ai: true` ou com um objeto `ai` que não declare
64
+ `enabled: false`, para que o modelo possa chamá-la. Então `ctx.ai.run(prompt)` executa o ciclo do
65
+ agente sobre as actions expostas: o modelo escolhe uma
66
+ ferramenta, o runtime executa a action como o usuário autenticado, limitado por `ctx.can`, e o
67
+ resultado volta ao modelo. O ciclo se repete até o modelo responder em texto.
70
68
 
71
69
  ```ts
72
70
  // A action opta por entrar; o schema do contrato também descreve a ferramenta:
73
- export const buscarNotas = defineContract({
74
- name: 'nota.buscar',
71
+ export const searchInvoices = defineContract({
72
+ name: 'invoice.search',
75
73
  kind: 'list',
76
74
  label: 'Buscar notas',
77
- input: z.object({ cliente: z.string(), mes: z.string() }),
78
- output: z.object({ numero: z.string(), valor: z.number() }), // em `list`, o schema de cada item
75
+ input: z.object({ customer: z.string(), month: z.string() }),
76
+ output: z.object({ number: z.string(), amount: z.number() }), // em `list`, o schema de cada item
79
77
  ai: { enabled: true, description: 'Busca notas por cliente e mês.' },
80
78
  })
81
79
 
82
80
  // No handler — ou fora dele, via runtime.aiFor(base), para o backend de um chat:
83
81
  const { text } = await ctx.ai!.run('Quais notas a Empresa X emitiu em junho?')
84
- // o modelo chamou nota.buscar sozinho, como o usuário logado, e respondeu em texto.
82
+ // o modelo chamou invoice.search sozinho, como o usuário logado, e respondeu em texto.
85
83
  ```
86
84
 
87
85
  `runtime.aiTools()` projeta cada action exposta como um `AiTool`: `name` é o identificador técnico
88
86
  usado na execução e na auditoria; `title` traz a `label` legível da action, quando existe;
89
87
  `description` vem de `ai.description` (ou da `description` da action) e `inputSchema`, do input.
90
- `metadata.dataProducts` lista os Produtos de Dados relacionados e `metadata.dataProductLabels`
91
- associa cada identificador ao seu nome legível. Esses metadados servem ao host: não entram no
88
+ `metadata.dataProducts` lista os [Produtos de Dados](/docs/data-products) relacionados e
89
+ `metadata.dataProductLabels` associa cada identificador ao seu nome legível. Esses metadados servem ao host: não entram no
92
90
  prompt e não concedem acesso.
93
91
 
94
92
  O agente só executa nomes que `aiTools()` anunciou; uma chamada a outra action volta como erro
95
93
  para o modelo. Actions marcadas com `ai: { destructive: true }` ou `ai: { requiresConfirmation: true }`
96
- **não rodam sem aprovação**: passe `run(prompt, { confirm })` — o chat mostra o "confirmar?"; sem
97
- isso, a action é recusada e o modelo avisa. O gate lê somente esses dois campos de `ai`: o
94
+ não rodam sem aprovação: passe `run(prompt, { confirm })` para que o chat peça a confirmação. Sem
95
+ `confirm`, a action é recusada e o modelo informa a recusa. O gate lê somente esses dois campos de `ai`: o
98
96
  `confirm: { destructive: true }` da action configura a confirmação da UI e não protege a chamada
99
- feita pelo agente. Fora do handler, `runtime.aiFor(base)` devolve o `ai` já ligado a um contexto —
100
- o backend do chat resolve o usuário e chama `.run(historico)`.
97
+ feita pelo agente. Fora do handler, `runtime.aiFor(base)` devolve o `ai` já ligado a um contexto:
98
+ o backend do chat resolve o usuário e chama `.run(history)`. Para agentes fora da aplicação, veja
99
+ [MCP](/docs/mcp).
101
100
 
102
101
  Para o modelo perguntar algo à pessoa no meio do turno, passe `onAsk` em `run` ou `runStream`. O
103
102
  driver Anthropic então oferece a tool reservada `ask_user`, entrega as perguntas (`AskQuestion[]`)
@@ -105,13 +104,13 @@ ao seu `onAsk` e devolve as respostas (`AskAnswer[]`) ao modelo. Sem `onAsk`, um
105
104
  `ask_user` é recusada com a orientação de perguntar em texto. Na interface, o componente
106
105
  [Ask](/ui/ask) apresenta essas perguntas e coleta as respostas.
107
106
 
108
- ## Streaming — o protocolo de eventos de conversa
107
+ ## Streaming da conversa
109
108
 
110
- `runStream` é o mesmo loop agêntico do `run`, emitindo **`ChatEvent`** conforme acontece
111
- (o contrato: `docs/chat-event-protocol.md`): `text` (delta incremental), `tool` (a action
112
- em uso — dado cru; humanizar é da apresentação), `artifact` (algo produzido na conversa)
113
- e `done`. Aditivo: o `run` clássico permanece, e `Promise<string>` segue sendo o caso
114
- degenerado do protocolo (um `text` + um `done`).
109
+ `runStream` executa o mesmo ciclo do `run` e emite `ChatEvent` à medida que ele avança. O protocolo
110
+ está em `docs/chat-event-protocol.md`, dentro do pacote: `text` traz um trecho novo de texto,
111
+ `tool` identifica a action em uso (sem tradução para a interface, que fica com quem apresenta),
112
+ `artifact` entrega algo produzido na conversa e `done` encerra o turno. O `run` continua disponível;
113
+ uma resposta completa equivale a um `text` seguido de `done`.
115
114
 
116
115
  ```ts
117
116
  // O backend de um chat streamando por SSE:
@@ -120,12 +119,12 @@ for await (const event of runtime.aiFor(base)!.runStream(messages)) {
120
119
  }
121
120
  ```
122
121
 
123
- O driver Anthropic emite os deltas token a token quando o client suporta `messages.stream`;
124
- com um client só-`create` (ex.: fake de teste), o texto de cada passo sai como delta único.
125
- O `<Chat>` do opus/ui consome os dois modos — ver a página **Chat**.
122
+ O driver Anthropic emite o texto em trechos, à medida que chega, quando o client oferece
123
+ `messages.stream`. Com um client que só tem `create`, como um client falso de teste, o texto de cada
124
+ passo sai num único trecho. O componente [Chat](/ui/chat) consome os dois modos.
126
125
 
127
- ## Limites (por enquanto)
126
+ ## Limites atuais
128
127
 
129
- O `run`/`runStream` fiam histórico multi-turn, mas sem memória longa/resumo automático.
130
- `extract` exige schema com objeto na raiz (`z.object`) — regra da API de tools do provider
131
- e o formato natural de um contrato.
128
+ `run` e `runStream` aceitam o histórico da conversa, mas não mantêm memória longa nem resumem o
129
+ histórico automaticamente. `extract` exige um schema com objeto na raiz (`z.object`), uma regra da
130
+ API de ferramentas do provedor.