@softize/opus 18.1.0 → 18.1.1

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 (96) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/PROMOTED.md +4 -5
  3. package/README.md +5 -4
  4. package/bin/cli.mjs +4 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +3 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
  8. package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
  9. package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
  10. package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
  11. package/docs/adr/{0016-productive-surfaces-use-compact-density.md → 0019-productive-surfaces-use-compact-density.md} +4 -1
  12. package/docs/code-style.md +2 -2
  13. package/docs/consumer-upgrade-propagation.md +1 -1
  14. package/docs/data-products.md +5 -3
  15. package/docs/protocol.md +6 -6
  16. package/docs/releasing.md +28 -4
  17. package/package.json +1 -1
  18. package/registry/skills/build-opus-ui/references/ui-patterns.md +11 -1
  19. package/src/auth/drivers/jwt.ts +2 -1
  20. package/src/core/runtime.ts +25 -4
  21. package/src/core/types.ts +4 -4
  22. package/src/mcp/index.ts +9 -0
  23. package/src/ui/components/patterns/content-header.tsx +1 -1
  24. package/src/ui/components/patterns/form.tsx +1 -1
  25. package/src/ui/components/patterns/sidebar.tsx +1 -1
  26. package/src/ui/components/primitives/card.tsx +1 -1
  27. package/src/ui/components/primitives/detail.tsx +7 -7
  28. package/src/ui/components/primitives/radio-group.tsx +1 -1
  29. package/src/ui/components/primitives/select.tsx +1 -1
  30. package/src/ui/docs/content/action-form-dialog.md +11 -4
  31. package/src/ui/docs/content/action-form.md +7 -16
  32. package/src/ui/docs/content/action-list-dialog.md +4 -6
  33. package/src/ui/docs/content/action-list.md +46 -4
  34. package/src/ui/docs/content/action-trigger.md +9 -5
  35. package/src/ui/docs/content/action-view.md +12 -8
  36. package/src/ui/docs/content/actions.md +36 -13
  37. package/src/ui/docs/content/ai.md +26 -7
  38. package/src/ui/docs/content/alert.md +4 -3
  39. package/src/ui/docs/content/aspect-ratio.md +2 -2
  40. package/src/ui/docs/content/auth.md +25 -10
  41. package/src/ui/docs/content/avatar.md +1 -1
  42. package/src/ui/docs/content/badge.md +2 -2
  43. package/src/ui/docs/content/breadcrumb.md +3 -2
  44. package/src/ui/docs/content/button.md +31 -7
  45. package/src/ui/docs/content/calendar.md +1 -1
  46. package/src/ui/docs/content/card.md +1 -1
  47. package/src/ui/docs/content/carousel.md +14 -3
  48. package/src/ui/docs/content/chat.md +1 -1
  49. package/src/ui/docs/content/cli.md +13 -7
  50. package/src/ui/docs/content/command.md +34 -2
  51. package/src/ui/docs/content/composer.md +1 -1
  52. package/src/ui/docs/content/content.md +5 -4
  53. package/src/ui/docs/content/customization.md +1 -1
  54. package/src/ui/docs/content/cycle.md +7 -5
  55. package/src/ui/docs/content/data-state.md +6 -5
  56. package/src/ui/docs/content/data.md +3 -3
  57. package/src/ui/docs/content/detail.md +3 -2
  58. package/src/ui/docs/content/dialog.md +2 -2
  59. package/src/ui/docs/content/dictionary-value.md +1 -1
  60. package/src/ui/docs/content/dock.md +23 -2
  61. package/src/ui/docs/content/dot.md +0 -2
  62. package/src/ui/docs/content/drawer.md +1 -1
  63. package/src/ui/docs/content/empty.md +1 -4
  64. package/src/ui/docs/content/events.md +1 -1
  65. package/src/ui/docs/content/field.md +20 -11
  66. package/src/ui/docs/content/getting-started.md +4 -2
  67. package/src/ui/docs/content/icon-picker.md +2 -2
  68. package/src/ui/docs/content/input-otp.md +2 -0
  69. package/src/ui/docs/content/input.md +2 -3
  70. package/src/ui/docs/content/item.md +5 -4
  71. package/src/ui/docs/content/kbd.md +2 -1
  72. package/src/ui/docs/content/mcp.md +10 -4
  73. package/src/ui/docs/content/menu.md +27 -0
  74. package/src/ui/docs/content/page.md +19 -5
  75. package/src/ui/docs/content/pagination.md +9 -2
  76. package/src/ui/docs/content/popover.md +2 -2
  77. package/src/ui/docs/content/presentation.md +46 -45
  78. package/src/ui/docs/content/progress.md +2 -6
  79. package/src/ui/docs/content/runtime.md +8 -5
  80. package/src/ui/docs/content/scheduler.md +1 -1
  81. package/src/ui/docs/content/select.md +13 -8
  82. package/src/ui/docs/content/sidebar.md +3 -2
  83. package/src/ui/docs/content/skeleton.md +1 -1
  84. package/src/ui/docs/content/slider.md +4 -4
  85. package/src/ui/docs/content/spinner.md +3 -3
  86. package/src/ui/docs/content/tabs.md +6 -6
  87. package/src/ui/docs/content/testing.md +4 -2
  88. package/src/ui/docs/content/toast.md +3 -5
  89. package/src/ui/docs/content/toggle.md +37 -0
  90. package/src/ui/docs/content/tooltip.md +4 -3
  91. package/src/ui/docs/content/truncate.md +3 -2
  92. package/src/ui/docs/content/ui.md +3 -1
  93. package/src/ui/docs/content/upgrading.md +43 -13
  94. package/src/ui/docs/doc-client.tsx +1 -1
  95. package/src/ui/docs/registry.tsx +30 -5
  96. package/src/ui/meta.ts +4 -4
@@ -19,8 +19,9 @@ superfície apenas no hover, preservando o fundo da região onde o botão está
19
19
  `size` usa a escala única dos controles: o mesmo nome tem a mesma medida em `Button`, `Select`,
20
20
  `Tabs`, `Toggle`, `Switch`, `Avatar`, `Spinner` e nos botões embutidos. Os tamanhos de texto dão a
21
21
  altura da linha; os `icon-*` são quadrados para botões só de ícone, que exigem `aria-label` porque não
22
- há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em `xs` e `sm`, 1rem em
23
- `default`, 1.25rem em `lg`), a menos que o ícone traga um `size-*` próprio.
22
+ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em `xs`, `sm`,
23
+ `icon-xs`, `icon-sm` e `icon`; 1rem em `default`; 1.25rem em `lg` e `icon-lg`), a menos que o
24
+ ícone traga um `size-*` próprio. O quadrado `icon` mantém a área de 2.25rem com o glifo discreto.
24
25
 
25
26
  | Nome | Medida | Uso |
26
27
  | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
@@ -36,8 +37,12 @@ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em
36
37
  Escolha o tamanho pela região, não pela importância visual: `variant` e `context` resolvem a
37
38
  hierarquia da ação. Headers de Page e footers de Dialog/Drawer usam `default`; ações operacionais
38
39
  de seção, toolbar e coleção usam `sm`; ações dentro de linha ou célula usam `xs` ou `icon-xs`.
39
- Controles de chrome, como voltar e fechar, usam `icon`. Assim a mesma decisão mantém a mesma
40
- altura mesmo quando uma superfície troca uma ação secundária por uma primária.
40
+ Assim a mesma decisão mantém a mesma altura mesmo quando uma superfície troca uma ação secundária
41
+ por uma primária.
42
+
43
+ O retorno de `PageBack` usa `icon`. O fechamento de Dialog e Drawer é um `CloseButton` circular
44
+ (`shape="pill"`), `neutral` e `subtle`, no quadrado `icon-sm` por padrão, ou `icon-xs` com
45
+ `closeSize="xs"`.
41
46
 
42
47
  ```tsx preview
43
48
  <Button size="xs">Mínimo</Button>
@@ -80,9 +85,10 @@ buttonVariants serve para o caso sem filho único.
80
85
 
81
86
  | Propriedade | Tipo | Padrão | Descrição |
82
87
  | ----------- | ------------------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
83
- | `context` | `'neutral' \| 'primary' \| 'danger'` | `'primary'` | A hierarquia ou o risco comunicado pela ação. |
88
+ | `context` | `'neutral' \| 'primary' \| 'danger'` | `'primary'` em `solid` e `subtle`; `'neutral'` nas demais variantes | A hierarquia ou o risco comunicado pela ação. |
84
89
  | `variant` | `'solid' \| 'subtle' \| 'outline' \| 'ghost' \| 'link'` | `'solid'` | O tratamento visual aplicado ao contexto. |
85
90
  | `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon' \| 'icon-lg'` | `'default'` | A medida na escala única dos controles (tabela acima). Os `icon-*` são quadrados para botões só de ícone, com `aria-label`. |
91
+ | `shape` | `'default' \| 'pill'` | `'default'` | Geometria do botão; `pill` arredonda as extremidades por completo. |
86
92
  | `asChild` | `boolean` | `false` | Renderiza como o filho (Radix Slot) em vez de `<button>` — para âncoras e afins. |
87
93
  | `busy` | `boolean` | `false` | Ação em andamento (depois do clique): mostra Spinner + desabilita. Não é "carregando" de conteúdo (que é Spinner/Skeleton em um nível de página). |
88
94
  | `icon` | `React.ReactNode` | | Ícone à esquerda, como nó (ex.: `icon={<Plus />}`); o glifo segue o `size`. No busy é trocado pelo Spinner — não soma. |
@@ -115,12 +121,30 @@ conectar bordas. Esse modo atende ações icon-only em barras e linhas de listag
115
121
  <Button variant="ghost" size="icon" aria-label="Exibição">
116
122
  <SlidersHorizontal />
117
123
  </Button>
118
- <Button context="primary" size="icon" aria-label="Novo item">
119
- <Plus />
124
+ <Button variant="ghost" size="icon" aria-label="Exportar">
125
+ <Download />
120
126
  </Button>
121
127
  </ButtonGroup>
122
128
  ```
123
129
 
130
+ ### No rodapé de Dialog e Drawer
131
+
132
+ `DialogFooter` e `DrawerFooter` cuidam somente da faixa; envolva as decisões em um
133
+ `ButtonGroup mode="spaced"`, que responde pelo agrupamento e pela distribuição. Por padrão, as
134
+ ações preservam a largura do conteúdo e `Cancelar` usa `ghost`. Use `distribution="equal"` somente
135
+ quando as duas decisões tiverem peso equivalente; nesse caso, `Cancelar` usa `outline`.
136
+
137
+ ```tsx
138
+ <DialogFooter>
139
+ <ButtonGroup mode="spaced">
140
+ <DialogClose asChild>
141
+ <Button variant="ghost">Cancelar</Button>
142
+ </DialogClose>
143
+ <Button>Salvar</Button>
144
+ </ButtonGroup>
145
+ </DialogFooter>
146
+ ```
147
+
124
148
  ### Ação dividida
125
149
 
126
150
  Combine a ação principal, um separador e um botão de ícone quando o mesmo comando oferecer
@@ -67,7 +67,7 @@ captionLayout=dropdown troca o título do mês por seletores de mês e ano — b
67
67
  | `locale` | `Locale` | `ptBR` | Idioma dos nomes de mês e de dia (um locale do date-fns). |
68
68
  | `labels` | `Partial<Labels>` | rótulos em pt-BR | Rótulos acessíveis da navegação e dos seletores; mescla sobre o padrão. |
69
69
 
70
- ## CalendarDayButton
70
+ ## Personalizar o dia com CalendarDayButton
71
71
 
72
72
  Cada dia é um `CalendarDayButton` — um `Button` ghost quadrado que recebe os modificadores do
73
73
  dia (`selected`, `range-start`, `today`…). Use `components={{ DayButton: … }}` para
@@ -72,4 +72,4 @@ Para painel com estrutura: cada slot é dono do próprio padding (como o Dialog)
72
72
  | Propriedade | Tipo | Padrão | Descrição |
73
73
  |---|---|---|---|
74
74
  | `asChild` | `boolean` | `false` | Renderiza o filho com a superfície do Card (ex.: um `<button>` clicável inteiro). |
75
- | `className` | `string` | | Compõe sobre a superfície; os slots (`CardHeader`, `CardBody`, `CardContent`, `CardFooter`, `CardAction`) são donos do próprio padding. |
75
+ | `className` | `string` | | Compõe sobre a superfície; os slots (`CardHeader`, `CardBody`, `CardFooter`, `CardAction`) são donos do próprio padding. `CardContent` é alias depreciado de `CardBody`. |
@@ -79,12 +79,23 @@ orientation=vertical empilha os slides; as setas migram para cima e para baixo (
79
79
  | Propriedade | Tipo | Padrão | Descrição |
80
80
  |---|---|---|---|
81
81
  | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Eixo do deslocamento. No modo vertical, os slides são empilhados e as setas apontam para cima e para baixo. |
82
- | `opts` | `CarouselOptions` | | Opções repassadas ao Embla, como `{ loop: true }` ou `{ align: 'start' }`. |
83
- | `setApi` | `(api: CarouselApi) => void` | | Recebe a instância para controle externo, como navegar com `scrollTo` ou ler o slide ativo. |
84
- | `plugins` | `CarouselPlugin[]` | | Plugins do Embla associados ao carrossel, como autoplay. |
82
+ | `opts` | `EmblaOptionsType` | | Opções repassadas ao Embla, como `{ loop: true }` ou `{ align: 'start' }`. O Opus não reexporta esse tipo; para nomeá-lo sem depender de `embla-carousel`, use `React.ComponentProps<typeof Carousel>['opts']`. |
83
+ | `setApi` | `(api: EmblaCarouselType \| undefined) => void` | | Recebe a instância do Embla para controle externo, como navegar com `scrollTo` ou ler o slide ativo. |
84
+ | `plugins` | `EmblaPluginType[]` | | Plugins do Embla associados ao carrossel, como autoplay. |
85
85
 
86
86
  ## Propriedades de CarouselItem
87
87
 
88
88
  | Propriedade | Tipo | Padrão | Descrição |
89
89
  |---|---|---|---|
90
90
  | `className` | `string` | | Classes de dimensão; a base define quantos slides cabem na área visível. |
91
+
92
+ ## Propriedades de CarouselPrevious e CarouselNext
93
+
94
+ Além das props abaixo, as setas aceitam as props de `Button`. O nome acessível já vem pronto:
95
+ “Slide anterior” e “Próximo slide”. O clique e o estado desabilitado acompanham a posição do trilho.
96
+
97
+ | Propriedade | Tipo | Padrão | Descrição |
98
+ |---|---|---|---|
99
+ | `variant` | `ButtonProps['variant']` | `'outline'` | Tratamento visual da seta. |
100
+ | `size` | `ButtonProps['size']` | `'icon'` | Escala do botão. A seta aplica `size-8 rounded-full` por cima da escala, então o círculo mede 2rem; use `className` para outra medida. |
101
+ | `className` | `string` | | Classes adicionais, como ajustar a posição fora do trilho. |
@@ -58,7 +58,7 @@ async function chatRoute(req) {
58
58
  <Chat send={async (messages) => (await api.post('/chat', { messages })).reply} />
59
59
  ```
60
60
 
61
- O modelofaz o que o usuário pode (`ctx.can`); o que é destrutivo pede `confirm`. Para expor as actions ao agente, marque-as com `ai: { enabled: true }` no contrato — ver o recurso **IA generativa**.
61
+ O agenteexecuta actions marcadas com `ai.enabled` e, nelas, aplica o `authorize` com o contexto do usuário; uma action sem `authorize` fica aberta a qualquer usuário autenticado. Actions com `ai.destructive` ou `ai.requiresConfirmation` são recusadas nesse exemplo, porque ele não passa `confirm`; para aprovar essas chamadas, use `run(messages, { confirm })`. Para expor as actions ao agente, marque-as com `ai: { enabled: true }` no contrato — ver o recurso **IA generativa**.
62
62
 
63
63
  ## Streaming — eventos de conversa
64
64
 
@@ -20,9 +20,11 @@ opus db check # drift entidade ↔ banco (read-only)
20
20
  opus seed check # bindings, dependências, ciclos e scripts paralelos de seed
21
21
  ```
22
22
 
23
- `opus check` lê o source sem executar nada e aplica nove regras: cinco sobre as actions
24
- (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`) e quatro sobre a UI
25
- (`ui-structure`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`). Um projeto
23
+ `opus check` lê o source sem executar nada e aplica regras em três grupos: actions
24
+ (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`), UI
25
+ (`ui-structure`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`) e Produtos de Dados
26
+ (`data-product-export`, `-id`, `-duplicate`, `-version`, `-interfaces`, `-interface`, `-entities`
27
+ e `-entity`, todas com o prefixo `data-product`). Um projeto
26
28
  marcado com `opus.json` e ainda sem actions passa vacuamente; sem o marcador, zero actions
27
29
  falha, porque um gate vazio não é aprovação. `opus check --help` descreve cada regra.
28
30
 
@@ -43,15 +45,19 @@ opus pre-push materialization # só a freshness dos artefatos materializados
43
45
  ## Seeds de desenvolvimento e teste
44
46
 
45
47
  > Seeds registrados no `opus.config.ts` têm perfis, métricas e escopos explícitos. Listagem e gate
46
- > não abrem o banco; planejamento, aplicação e verificação são bloqueados em produção.
48
+ > não abrem o banco; planejamento, aplicação e verificação conectam com
49
+ > `NODE_ENV=development` ou `NODE_ENV=test`.
47
50
 
48
51
  ```bash
49
52
  opus seed list
50
- opus seed plan customers.scenarios --profile smoke --scope local
51
- opus seed apply customers.scenarios --profile smoke --scope local
52
- opus seed verify customers.scenarios --profile smoke --scope local
53
+ NODE_ENV=development opus seed plan customers.scenarios --profile smoke --scope local
54
+ NODE_ENV=development opus seed apply customers.scenarios --profile smoke --scope local
55
+ NODE_ENV=development opus seed verify customers.scenarios --profile smoke --scope local
53
56
  ```
54
57
 
58
+ Com qualquer outro valor de `NODE_ENV`, inclusive ausente, esses três comandos falham antes de
59
+ abrir a conexão. O escopo informado também precisa constar em `safety.scopes` do seed.
60
+
55
61
  `--profile` escolhe o perfil (default: o `defaultProfile` do seed) e `--scope` declara o escopo
56
62
  dos dados (a variável `OPUS_SEED_SCOPE` é a alternativa). `--json` devolve o resultado
57
63
  estruturado, inclusive em caso de erro. `apply` converge quando repetido; não há reset ou
@@ -49,14 +49,36 @@ render(
49
49
  )
50
50
  ```
51
51
 
52
+ ## Propriedades de Command
53
+
54
+ | Propriedade | Tipo | Padrão | Descrição |
55
+ |---|---|---|---|
56
+ | `shouldFilter` | `boolean` | `true` | Com `false`, desliga a filtragem e a ordenação automáticas; o aplicativo passa a renderizar somente os itens que correspondem à busca. |
57
+ | `filter` | `(value: string, search: string, keywords?: string[]) => number` | | Pontua cada item para a busca atual, de `0` (oculto) a `1` (melhor resultado). |
58
+ | `value` | `string` | | Item destacado no modo controlado. |
59
+ | `onValueChange` | `(value: string) => void` | | Chamado quando o item destacado muda. |
60
+ | `loop` | `boolean` | `false` | Faz as setas voltarem ao início ou ao fim da lista. |
61
+ | `label` | `string` | | Nome acessível do menu, sem exibição visual. |
62
+ | `className` | `string` | | Classes adicionais aplicadas à raiz. |
63
+
64
+ ## Propriedades de CommandInput
65
+
66
+ | Propriedade | Tipo | Padrão | Descrição |
67
+ |---|---|---|---|
68
+ | `placeholder` | `string` | | Texto exibido enquanto a busca está vazia. |
69
+ | `value` | `string` | | Texto da busca no modo controlado. |
70
+ | `onValueChange` | `(search: string) => void` | | Chamado quando o texto da busca muda. |
71
+ | `disabled` | `boolean` | `false` | Desabilita o campo de busca. |
72
+ | `className` | `string` | | Classes adicionais aplicadas ao campo; o ícone de busca fica no wrapper. |
73
+
52
74
  ## Propriedades de CommandDialog
53
75
 
54
76
  | Propriedade | Tipo | Padrão | Descrição |
55
77
  |---|---|---|---|
56
78
  | `open` | `boolean` | | Estado do modal no modo controlado. |
57
79
  | `onOpenChange` | `(open: boolean) => void` | | Atualiza o estado do modal; pode ser conectado ao atalho do aplicativo. |
58
- | `title` | `string` | `'Comandos'` | Nome acessível do diálogo, disponível para leitura assistiva. |
59
- | `description` | `string` | `'Busque um comando para executar.'` | Descrição acessível do diálogo. |
80
+ | `title` | `string` | `'Paleta de comandos'` | Título exibido no header compacto; também nomeia o diálogo para tecnologias assistivas. |
81
+ | `description` | `string` | `'Busque um comando para executar'` | Descrição disponível somente para tecnologias assistivas. |
60
82
  | `showCloseButton` | `boolean` | `true` | Exibe o botão de fechamento. |
61
83
 
62
84
  ## Propriedades de CommandItem
@@ -64,3 +86,13 @@ render(
64
86
  | Propriedade | Tipo | Padrão | Descrição |
65
87
  |---|---|---|---|
66
88
  | `onSelect` | `(value: string) => void` | | Chamado ao selecionar o item por clique ou teclado. |
89
+ | `value` | `string` | | Valor estável usado na busca e no destaque. Sem ele, o valor é inferido do texto do item. |
90
+ | `keywords` | `string[]` | | Termos adicionais considerados na busca. |
91
+ | `disabled` | `boolean` | `false` | Impede a seleção do item. |
92
+
93
+ ## Propriedades de CommandGroup
94
+
95
+ | Propriedade | Tipo | Padrão | Descrição |
96
+ |---|---|---|---|
97
+ | `heading` | `React.ReactNode` | | Título exibido acima dos itens do grupo. |
98
+ | `value` | `string` | | Identificador do grupo; obrigatório e único quando não há `heading`. |
@@ -1,6 +1,6 @@
1
1
  ## Envio de texto
2
2
 
3
- A caixa de escrever da casa: textarea em uma pílula elevada (`rounded-xl` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/components/chat) extraído — use sozinho quando há entrada de texto mas não um chat (ex.: criar uma sessão). Controlado: o dono do texto é você. Os callbacks opcionais `onHistoryPrevious` e `onHistoryNext` permitem que esse dono consuma **↑/↓**; sem eles, as setas mantêm o comportamento nativo da textarea.
3
+ A caixa de escrever da casa: textarea em uma pílula elevada (`rounded-xl` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/ui/chat) extraído — use sozinho quando há entrada de texto mas não um chat (ex.: criar uma sessão). Controlado: o dono do texto é você. Os callbacks opcionais `onHistoryPrevious` e `onHistoryNext` permitem que esse dono consuma **↑/↓**; sem eles, as setas mantêm o comportamento nativo da textarea.
4
4
 
5
5
  ```tsx preview col
6
6
  const [text, setText] = React.useState('')
@@ -14,7 +14,7 @@ render(
14
14
  actions={<Button variant="outline">Encerrar outras sessões</Button>}
15
15
  >
16
16
  <div className="rounded-lg border border-border p-4">
17
- MacBook Para o · ativo agora
17
+ MacBook Pro · ativo agora
18
18
  </div>
19
19
  </Content>,
20
20
  );
@@ -40,7 +40,7 @@ render(
40
40
  </ContentHeader>
41
41
  <ContentBody>
42
42
  <div className="rounded-lg border border-border p-4">
43
- MacBook Para o · ativo agora
43
+ MacBook Pro · ativo agora
44
44
  </div>
45
45
  </ContentBody>
46
46
  </Content>,
@@ -57,7 +57,8 @@ Quando a região principal da página é uma coleção, `Content` nomeia e gover
57
57
  `ContentActions`. Busca, filtros, atualização e operações dependentes do recorte atual permanecem na
58
58
  toolbar da lista. Essa divisão aproxima cada comando do objeto que ele afeta sem criar uma família
59
59
  paralela de componentes `List*`. Na variante `page`, as ações ficam no extremo oposto ao título. A
60
- criação usa um botão textual `default`, sem ícone, nomeado `Criar recurso`; o diálogo aberto pelo
60
+ criação usa um `Button` textual na variante padrão (`solid`) e no tamanho `default`, sem ícone,
61
+ nomeado `Criar recurso`; o diálogo aberto pelo
61
62
  gatilho repete esse título e a edição usa `Editar recurso`.
62
63
 
63
64
  ## Propriedades de Content
@@ -68,7 +69,7 @@ gatilho repete esse título e a edição usa `Editar recurso`.
68
69
  | `count` | `number` | | Forma curta: total de itens ao lado do título da região. |
69
70
  | `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. |
70
71
  | `actions` | `ReactNode` | | Forma curta: ações sobre a região inteira; em `page`, ficam no extremo oposto ao título. |
71
- | `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | herdado | Nível semântico do heading, independente do destaque visual. |
72
+ | `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível semântico do heading, independente do destaque visual. |
72
73
  | `variant` | `'page' \| 'section'` | `'section'` | Hierarquia visual: `page` reproduz o cabeçalho de `Page`; `section` é a região dentro de uma superfície. |
73
74
 
74
75
  Na composição explícita, `ContentHeader` recebe `ContentTitle`, `ContentMeta`, `ContentDescription` e
@@ -116,7 +116,7 @@ const [open, setOpen] = useState(false)
116
116
 
117
117
  ## O limite — de propósito
118
118
 
119
- > O look curado não é customizável no app: rodapé-faixa do dialog, elevação no dark, `active:scale`
119
+ > O look curado não é customizável no app: rodapé-faixa do dialog, elevação no dark, `active:brightness-90`
120
120
  > do botão, a seta do tooltip — é identidade da casa, igual em todo projeto.
121
121
 
122
122
  ```tsx preview
@@ -4,12 +4,14 @@ O Opus cresce por **reincidência, nunca por especulação**. Quando o mesmo com
4
4
 
5
5
  ## Onde você aponta
6
6
 
7
- No seu projeto, uma linha por apontamento em `.opus/issues.jsonl` na raiz do repo. Dois tipos:
7
+ Cada apontamento vira uma Issue no repositório do Opus. Antes de abrir, procure uma Issue existente para o mesmo comportamento e comente nela. Dois tipos:
8
8
 
9
9
  - **`enhancement`** — código genérico que o Opus deveria ganhar (serviria a qualquer projeto). Antes de apontar, confira o que o Opus já tem (o inventário de componentes, os exports, esta doc): se já existe, use.
10
10
  - **`bug`** — um defeito ou limitação no próprio Opus, achado usando-o.
11
11
 
12
- ```json
12
+ Descreva cada apontamento com estes campos — um exemplo de cada tipo:
13
+
14
+ ```text
13
15
  {"type": "enhancement", "behavior": "o comportamento em uma frase.", "file": "src/onde-vive.ts", "kind": "ui|sdk|driver|infra"}
14
16
  {"type": "bug", "behavior": "o que quebra ou falta.", "file": "onde se manifesta.", "note": "repro + o workaround aplicado."}
15
17
  ```
@@ -20,15 +22,15 @@ O ponto do ciclo é **não bloquear a entrega**. Bateu em um gap ou em um bug do
20
22
 
21
23
  1. **Contorne local** — componha um wrapper no seu projeto. O Opus entrega _source_, então dá para embrulhar qualquer superfície dele. Nunca edite `node_modules` (some no próximo install).
22
24
  2. **Entregue** a feature com o workaround.
23
- 3. **Aponte** no `.opus/issues.jsonl` e siga em frente.
25
+ 3. **Aponte** na Issue e siga em frente.
24
26
 
25
27
  O "depois" — o conserto no Opus — corre em paralelo. Ele não segura o seu trabalho; o único custo de demorar é o workaround viver um pouco mais.
26
28
 
27
29
  ## Como fecha
28
30
 
29
- O apontamento é colhido e abre uma **Issue no repositório do Opus**, onde a triagem acontece (deduplicada re-apontar o mesmo é idempotente):
31
+ A triagem acontece em uma **Issue em [github.com/softize-dev/opus](https://github.com/softize-dev/opus/issues)**; procure uma existente antes de abrir. O pacote não coleta apontamentos do seu projeto por conta própria. O desfecho depende do tipo:
30
32
 
31
- - **Enhancement** aceito (com reincidência) → implementado no Opus → sai em um _bump_ → seu projeto atualiza o pin (`opus.json`) e troca o workaround pelo import.
33
+ - **Enhancement** aceito (com reincidência) → implementado no Opus → sai em um _bump_ → seu projeto [atualiza o Opus](upgrading) e troca o workaround pelo import.
32
34
  - **Bug** → vira _fix_ + entrada no `CHANGELOG` → no _bump_, o workaround sai.
33
35
 
34
36
  A régua e a decisão ficam com quem mantém o Opus — hoje, a **Softize**. O registro curado das promoções vive no `PROMOTED.md` do pacote.
@@ -15,7 +15,7 @@ e o tooltip dessa ação. Para uma ação em andamento depois do clique, use `bu
15
15
  <DataState empty emptyMessage="Nenhum papel.">
16
16
  <div />
17
17
  </DataState>
18
- <DataState error={{ message: 'detalhe técnico fica no console' }} onRetry={() => undefined}>
18
+ <DataState error={{ message: 'detalhe técnico que não aparece na tela' }} onRetry={() => undefined}>
19
19
  <div />
20
20
  </DataState>
21
21
  </div>
@@ -28,15 +28,15 @@ tabela continua responsável pela borda: o `Empty` dentro dela vem sem moldura.
28
28
  disponível para criação ou vínculo, use `Empty` diretamente, com a moldura tracejada.
29
29
 
30
30
  ```tsx preview col
31
- <table className="w-full overflow-hidden rounded-lg border border-border text-sm">
32
- <tbody>
31
+ <Table variant="framed">
32
+ <TableBody>
33
33
  <DataState empty emptyMessage="Nenhum usuário." colSpan={3}>
34
34
  <tr>
35
35
  <td />
36
36
  </tr>
37
37
  </DataState>
38
- </tbody>
39
- </table>
38
+ </TableBody>
39
+ </Table>
40
40
  ```
41
41
 
42
42
  ## Propriedades de DataState
@@ -51,3 +51,4 @@ disponível para criação ou vínculo, use `Empty` diretamente, com a moldura t
51
51
  | `onRetry` | `() => void \| Promise<void>` | | Recuperação: mostra a ação de tentar de novo somente com ícone no estado de erro. |
52
52
  | `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
53
53
  | `colSpan` | `number` | | Em tabela: renderiza o estado como `<tr><td colSpan>` (cabe direto no tbody). |
54
+ | `children` | `ReactNode` | | Obrigatório. Conteúdo renderizado quando não há erro, carregamento nem vazio. |
@@ -48,15 +48,15 @@ const db = new Kysely<AdminDB>({
48
48
  await db.selectFrom('agents').select(['isDefault', 'roleId']).where('workspaceId', '=', id).execute()
49
49
  ```
50
50
 
51
- ## Migrações, preparação e seeds
51
+ ## Migrações e seeds
52
52
 
53
53
  Cada etapa possui um papel diferente:
54
54
 
55
55
  - `opus db migrate` — aplica o **schema idempotente** (`config.schema`, um script SQL
56
56
  evolutivo: `IF NOT EXISTS` + guards cobrem nascer do zero e upgrade no mesmo artefato)
57
57
  e roda o drift-check entidade ↔ banco na sequência (exit ≠ 0 se divergir).
58
- - `prepare` — backfill estrutural, prod-safe e idempotente; roda no deploy (depois do migrate).
59
- - `seed` cria dados de desenvolvimento e teste. Não é executado em produção.
58
+ - `opus seed` — aplica datasets de desenvolvimento e teste declarados com `defineSeed` e registrados
59
+ no `opus.config.ts`; conecta com `NODE_ENV=development` ou `test`. Ver [CLI opus](cli).
60
60
 
61
61
  ## Campos JSON
62
62
 
@@ -38,7 +38,8 @@ React substitui a apresentação. `0` e `false` seguem como valores. Ver `EmptyV
38
38
 
39
39
  ## Moldura e divisórias internas
40
40
 
41
- `variant="framed"` adiciona a superfície e a borda externa. `dividers` desenha apenas as
41
+ `variant="framed"` adiciona a borda externa sem preencher o fundo; na orientação horizontal, só a
42
+ coluna de rótulos recebe um fundo sutil. `dividers` desenha apenas as
42
43
  divisórias internas; as duas opções são independentes e podem ser combinadas. `orientation`
43
44
  define se a chave fica sobre o valor ou ao lado dele. Na orientação horizontal, todos os valores
44
45
  começam depois da mesma coluna de rótulo, com largura padrão de `7rem` em grupos simples e
@@ -62,7 +63,7 @@ começam depois da mesma coluna de rótulo, com largura padrão de `7rem` em gru
62
63
 
63
64
  | Propriedade | Tipo | Padrão | Descrição |
64
65
  |---|---|---|---|
65
- | `variant` | `'plain' \| 'framed'` | `'plain'` | `framed` aplica a superfície e a moldura canônicas ao conjunto. |
66
+ | `variant` | `'plain' \| 'framed'` | `'plain'` | `framed` aplica a moldura canônica ao conjunto, com fundo transparente; na orientação horizontal, a coluna de rótulos recebe fundo sutil. |
66
67
  | `dividers` | `boolean` | `false` | Hairlines somente entre os campos, sem exigir moldura externa. |
67
68
  | `columns` | `1 \| 2 \| 3 \| 4 \| 'auto'` | `1` | Número fixo de colunas ou distribuição automática por largura mínima. |
68
69
  | `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Chave sobre o valor ou ao lado dele em cada campo. |
@@ -37,7 +37,7 @@ distinta da página.
37
37
  <DialogClose asChild>
38
38
  <Button variant="ghost">Cancelar</Button>
39
39
  </DialogClose>
40
- <Button>Criar workspace</Button>
40
+ <Button>Salvar</Button>
41
41
  </ButtonGroup>
42
42
  </DialogFooter>
43
43
  </DialogContent>
@@ -149,7 +149,7 @@ confirmações e prompts distribuem confirmar e cancelar em duas colunas.
149
149
  Em `dialog.choose`, declare pelo menos uma ação e marque exatamente uma ação habilitada com
150
150
  `initialFocus`. Essa ação representa a saída segura. Os valores de `result` precisam ser únicos.
151
151
  Por padrão, a última ação recebe contexto primário e variante sólida; as anteriores usam contexto
152
- neutro e variante ghost. Defina `context` ou `variant` na própria ação para substituir o padrão.
152
+ neutro e variante outline. Defina `context` ou `variant` na própria ação para substituir o padrão.
153
153
 
154
154
  ### Montar o host
155
155
 
@@ -120,7 +120,7 @@ resultado é vazio e a resolução cai na meta do `t.dict` que viaja no schema.
120
120
  | `dict` | `DictType \| LogicalTypeMeta \| DictionaryDescriptor` | — | O dicionário (`t.dict`), a meta lida do schema ou um descritor normalizado. |
121
121
  | `value` | `string \| null \| undefined` | — | O código. Vazio renderiza `fallback`. |
122
122
  | `presentation` | `'classification' \| 'status' \| 'stage' \| 'plain'` | do dicionário | Sobrepõe o papel só nesta ocorrência. |
123
- | `context` | contexto de `Badge` | pelo dicionário | Sobrepõe o significado semântico e promove texto a badge. |
123
+ | `context` | contexto de `Badge` | pelo dicionário | Sobrepõe o significado semântico quando a ocorrência já é badge; sozinho, não promove texto a badge. |
124
124
  | `variant` | variante de `Badge` | pelo papel | Sobrepõe a variante e promove texto a badge. |
125
125
  | `icon` | `boolean \| ReactNode` | `true` | `false` esconde o ícone declarado; um nó substitui. |
126
126
  | `icons` | `Record<string, LucideIcon>` | `iconPickerIcons` | Catálogo nome → componente. |
@@ -1,5 +1,3 @@
1
- # Dock
2
-
3
1
  Ferramentas ancoradas à superfície de trabalho. Um canvas ou um editor raramente comporta mais
4
2
  uma faixa de chrome no topo: a trilha da aplicação já ocupa esse papel, e um segundo cabeçalho
5
3
  empilha duas faixas com a mesma função. A Dock coloca as ações sobre a própria superfície,
@@ -63,10 +61,33 @@ A barra é uma `toolbar`: as setas andam entre as ações e Home/End vão às po
63
61
  por tooltip, então `<TooltipProvider>` precisa existir na raiz do app — o esqueleto do `opus create`
64
62
  já monta.
65
63
 
64
+ ## Propriedades de Dock
65
+
66
66
  | Propriedade | Tipo | Padrão | Descrição |
67
67
  | --- | --- | --- | --- |
68
68
  | `position` | `'bottom' \| 'bottom-left' \| 'bottom-right'` | `'bottom'` | Aresta do contêiner onde a barra se ancora. |
69
69
  | `label` | `string` | | Nome acessível da barra. |
70
+ | `children` | `ReactNode` | | Os grupos de ações, em `DockGroup`. |
71
+ | `className` | `string` | | Classes adicionais da barra. |
72
+
73
+ ## Propriedades de DockGroup
74
+
75
+ | Propriedade | Tipo | Padrão | Descrição |
76
+ | --- | --- | --- | --- |
77
+ | `children` | `ReactNode` | | As ações do grupo, em `DockAction`. A divisória antes do grupo é aplicada automaticamente, exceto no primeiro. |
78
+ | `className` | `string` | | Classes adicionais do grupo. |
79
+
80
+ ## Propriedades de DockAction
81
+
82
+ | Propriedade | Tipo | Padrão | Descrição |
83
+ | --- | --- | --- | --- |
84
+ | `icon` | `ReactNode` | | Ícone exibido no botão. |
85
+ | `label` | `string` | | Nome da ação; vira o nome acessível e o tooltip quando não há `hint`. |
86
+ | `pressed` | `boolean` | | Presente quando a ação liga um modo: aplica o preenchimento e expõe `aria-pressed`. Ausente quando a ação apenas executa. |
87
+ | `disabled` | `boolean` | `false` | Desabilita a ação e a tira da navegação por setas. |
88
+ | `hint` | `ReactNode` | `label` | Conteúdo do tooltip quando ele precisa dizer mais que o rótulo. |
89
+ | `onClick` | `() => void` | | Executa a ação ou alterna o modo. |
90
+ | `className` | `string` | | Classes adicionais do botão. |
70
91
 
71
92
  ## Propriedades de SurfaceStatus
72
93
 
@@ -1,5 +1,3 @@
1
- # Dot
2
-
3
1
  Indicador visual compacto para estados que já têm contexto. O tamanho permanece fixo; `context`
4
2
  seleciona o significado e `variant` escolhe ponto sólido ou contornado. Quando a cor reforça significado, `label` fornece o nome
5
3
  acessível. Sem `label`, o ponto é decorativo.
@@ -11,7 +11,7 @@ entra pela direita e pode ser fechado por Esc, pelo overlay ou pelo botão de fe
11
11
  </DrawerTrigger>
12
12
  <DrawerContent side="right">
13
13
  <DrawerHeader>
14
- <DrawerTitle>criarTicket</DrawerTitle>
14
+ <DrawerTitle>Criar ticket</DrawerTitle>
15
15
  </DrawerHeader>
16
16
  <DrawerBody className="text-sm text-muted-foreground">
17
17
  <p>Ação · domínio Suporte</p>
@@ -43,10 +43,7 @@ título e descrição agrupados no centro.
43
43
  </EmptyDescription>
44
44
  </EmptyHeader>
45
45
  <EmptyActions>
46
- <Button>
47
- <Plus />
48
- Anexar skill
49
- </Button>
46
+ <Button icon={<Plus />}>Anexar skill</Button>
50
47
  </EmptyActions>
51
48
  </Empty>
52
49
  ```
@@ -46,7 +46,7 @@ const runtime = createRuntime({
46
46
  // No handler: emite pelo tipo declarado; o core preenche id/timestamp/actor/source.
47
47
  handler: async (ctx, input) => {
48
48
  const nota = await repo.create(input)
49
- ctx.emit('nota.emitida', { id: nota.id, valor: nota.valor })
49
+ await ctx.emit('nota.emitida', { id: nota.id, valor: nota.valor })
50
50
  return nota
51
51
  }
52
52
  ```
@@ -1,29 +1,33 @@
1
1
  ## Campo empilhado
2
2
 
3
3
  Use `Field` para manter rótulo, controle, ajuda e erro com espaçamento consistente. Na orientação
4
- vertical, `FieldLabel`, o controle e `FieldDescription` ficam empilhados. Quando o controle usa
5
- `aria-invalid`, o rótulo acompanha o contexto de erro.
4
+ vertical, `FieldLabel` e o controle ficam empilhados. A ajuda fica junto ao rótulo, com `LabelHelp`,
5
+ o mesmo ícone que o `ActionForm` usa para `FieldSpec.help`. Use `FieldDescription` somente quando a
6
+ frase precisar ficar visível o tempo todo abaixo do controle.
6
7
 
7
8
  ```tsx preview col md
8
9
  <Field>
9
- <FieldLabel htmlFor="workspace-slug">Slug do workspace</FieldLabel>
10
+ <FieldLabel htmlFor="workspace-slug" className="items-center gap-1.5">
11
+ <span>Slug do workspace</span>
12
+ <LabelHelp help="Vira o subdomínio do preview: empresa-x.preview.softize.com.br." />
13
+ </FieldLabel>
10
14
  <Input id="workspace-slug" defaultValue="empresa-x" />
11
- <FieldDescription>Vira o subdomínio do preview: empresa-x.preview.softize.com.br.</FieldDescription>
12
15
  </Field>
13
16
  ```
14
17
 
15
18
  ## Horizontal com toggle
16
19
 
17
20
  `orientation="horizontal"` posiciona o controle ao lado do texto. `FieldContent` agrupa título e
18
- descrição; `FieldTitle` nomeia o campo quando o texto não puder ser um `<label>`.
21
+ descrição; `FieldTitle` exibe o título quando o texto não puder ser um `<label>`. Como ele não se
22
+ vincula ao controle, ligue-o ao controle por `aria-labelledby` para manter o nome acessível.
19
23
 
20
24
  ```tsx preview col md
21
25
  <Field orientation="horizontal">
22
26
  <FieldContent>
23
- <FieldTitle>Acionar o revisor no handoff</FieldTitle>
27
+ <FieldTitle id="auto-review-title">Acionar o revisor no handoff</FieldTitle>
24
28
  <FieldDescription>Ao abrir o handoff, o agente reviewer entra na sessão automaticamente.</FieldDescription>
25
29
  </FieldContent>
26
- <Switch id="auto-review" defaultChecked />
30
+ <Switch id="auto-review" aria-labelledby="auto-review-title" defaultChecked />
27
31
  </Field>
28
32
  ```
29
33
 
@@ -31,21 +35,25 @@ descrição; `FieldTitle` nomeia o campo quando o texto não puder ser um `<labe
31
35
 
32
36
  `FieldSet` agrupa campos sob uma `FieldLegend`; `FieldGroup` define o espaçamento e
33
37
  `FieldSeparator` marca uma divisão. `FieldError` aparece somente quando existe mensagem de erro.
38
+ Declare `data-invalid="true"` no `Field` para que o rótulo acompanhe o contexto de erro;
39
+ `aria-invalid` marca apenas o controle. O `ActionForm` define os dois atributos sozinho.
34
40
 
35
41
  ```tsx preview col md
36
42
  <FieldSet>
37
43
  <FieldLegend>Novo agente</FieldLegend>
38
44
  <FieldGroup>
39
- <Field>
45
+ <Field data-invalid="true">
40
46
  <FieldLabel htmlFor="agent-name">Nome</FieldLabel>
41
47
  <Input id="agent-name" defaultValue="" aria-invalid placeholder="Ex.: developer" />
42
48
  <FieldError>Informe um nome para o agente.</FieldError>
43
49
  </Field>
44
50
  <FieldSeparator />
45
51
  <Field>
46
- <FieldLabel htmlFor="agent-prompt">Prompt</FieldLabel>
52
+ <FieldLabel htmlFor="agent-prompt" className="items-center gap-1.5">
53
+ <span>Prompt</span>
54
+ <LabelHelp help="A síntese vira a primeira mensagem da sessão." />
55
+ </FieldLabel>
47
56
  <Textarea id="agent-prompt" rows={3} defaultValue="Veste o papel de developer no workspace Empresa X." />
48
- <FieldDescription>A síntese vira a primeira mensagem da sessão.</FieldDescription>
49
57
  </Field>
50
58
  </FieldGroup>
51
59
  </FieldSet>
@@ -56,6 +64,7 @@ descrição; `FieldTitle` nomeia o campo quando o texto não puder ser um `<labe
56
64
  | Propriedade | Tipo | Padrão | Descrição |
57
65
  |---|---|---|---|
58
66
  | `orientation` | `'vertical' \| 'horizontal' \| 'responsive'` | `'vertical'` | Direção fixa do campo. `responsive` permanece como alias compatível de `horizontal`. |
67
+ | `data-invalid` | `boolean \| 'true' \| 'false'` | | Com `true`, aplica o contexto de erro ao campo e ao rótulo. O `ActionForm` define o atributo sozinho. |
59
68
 
60
69
  ## Propriedades de FieldLegend
61
70
 
@@ -67,7 +76,7 @@ descrição; `FieldTitle` nomeia o campo quando o texto não puder ser um `<labe
67
76
 
68
77
  | Propriedade | Tipo | Padrão | Descrição |
69
78
  |---|---|---|---|
70
- | `errors` | `Array<{ message?: string }>` | | Deduplica e apresenta uma lista de erros; sem conteúdo, o componente não renderiza. |
79
+ | `errors` | `Array<{ message?: string } \| undefined>` | | Deduplica e apresenta uma lista de erros; sem conteúdo, o componente não renderiza. |
71
80
  | `children` | `ReactNode` | | Mensagem literal, com prioridade sobre `errors`. |
72
81
 
73
82
  ## Propriedades de FieldSeparator
@@ -75,8 +75,10 @@ pnpm add @softize/opus
75
75
  pnpm add -D @softize/base
76
76
  pnpm exec opus setup
77
77
  pnpm exec base setup
78
+ ```
78
79
 
79
- # index.css do app — o tema canônico + os componentes do Opus no scan do Tailwind.
80
+ ```css
81
+ /* index.css do app — o tema canônico + os componentes do Opus no scan do Tailwind. */
80
82
  @import '@softize/opus/ui/theme.css';
81
83
  @source '../node_modules/@softize/opus/src/ui/**/*.{ts,tsx}';
82
84
  ```
@@ -111,4 +113,4 @@ await runtime.start()
111
113
 
112
114
  - `CLAUDE.md` do repo e do app — o índice local: mapa, gates, gotchas.
113
115
  - As `description` das entidades/actions — a spec de negócio, projetada no manifest pelo `opus gen`.
114
- - Gate de build: `opus check` (convenções das actions) + `opus db check` (drift entidade ↔ banco). Rode antes de entregar.
116
+ - Gate de build: `opus check` (convenções das actions e da UI) + `opus copy --check` e `base copy check` (inventário e política de copy) + `opus db check` (drift entidade ↔ banco). Rode antes de entregar.