@softize/opus 22.1.0 → 23.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 (52) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/bin/cli.mjs +6 -5
  3. package/bin/lib/check.mjs +23 -8
  4. package/bin/lib/copy.mjs +1 -1
  5. package/bin/lib/gen-manifest.mjs +2 -0
  6. package/bin/lib/gen-runner.mjs +7 -0
  7. package/bin/lib/gen-stubs.mjs +3 -1
  8. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
  9. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
  10. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
  11. package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
  12. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  13. package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
  14. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
  15. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +60 -0
  16. package/docs/adr/0041-actions-declare-effect-and-personal-data.md +139 -0
  17. package/docs/code-style.md +1 -1
  18. package/docs/protocol.md +15 -2
  19. package/package.json +1 -1
  20. package/registry/skills/build-opus-ui/references/ui-patterns.md +1 -1
  21. package/registry/skills/create-opus-action/SKILL.md +7 -2
  22. package/registry/skills/create-opus-action/references/contract-and-binding.md +10 -5
  23. package/registry/skills/create-opus-action/references/evaluations.md +3 -0
  24. package/registry/skills/create-opus-action/scripts/scaffold.mjs +6 -0
  25. package/registry/templates/app/src/domains/tasks/actions/list.ts +2 -0
  26. package/src/core/action-nature.ts +40 -0
  27. package/src/core/audit.ts +12 -4
  28. package/src/core/contracts.ts +1 -1
  29. package/src/core/index.ts +2 -0
  30. package/src/core/runtime.ts +57 -35
  31. package/src/core/types.ts +45 -23
  32. package/src/design/index.ts +14 -3
  33. package/src/mcp/index.ts +6 -0
  34. package/src/testing/index.ts +6 -26
  35. package/src/ui/components/patterns/content-header.tsx +5 -9
  36. package/src/ui/components/patterns/list.tsx +55 -1
  37. package/src/ui/components/patterns/presentation.tsx +1 -1
  38. package/src/ui/components/primitives/icon-picker.tsx +6 -0
  39. package/src/ui/components/primitives/tabs.tsx +55 -87
  40. package/src/ui/components/primitives/toggle.tsx +1 -1
  41. package/src/ui/docs/content/action-list.md +13 -4
  42. package/src/ui/docs/content/actions.md +36 -8
  43. package/src/ui/docs/content/content.md +5 -6
  44. package/src/ui/docs/content/mcp.md +6 -0
  45. package/src/ui/docs/content/semantic-context.md +0 -1
  46. package/src/ui/docs/content/tabs.md +68 -69
  47. package/src/ui/docs/content/testing.md +2 -3
  48. package/src/ui/docs/content/toggle.md +15 -10
  49. package/src/ui/drivers/react.tsx +4 -3
  50. package/src/ui/meta.ts +2 -2
  51. package/src/ui/react.tsx +2 -3
  52. package/src/vite/design.ts +23 -21
@@ -1,12 +1,11 @@
1
1
  import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
2
  import { Tabs as TabsPrimitive } from "radix-ui";
4
3
 
5
4
  import { cn } from "../../lib/cn.ts";
6
5
  import { controlHeight, type ControlSize } from "./control.ts";
7
6
 
8
7
  // Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
9
- // não tinha, e uma fileira densa ficava com o segmento 0.25rem mais alto
8
+ // não tinha, e uma fileira densa ficava com as abas 0.25rem mais altas
10
9
  // que os irmãos. O tamanho flui pra TabsList via contexto (a altura é dela) e a medida é a
11
10
  // da escala de control.ts.
12
11
  type TabsSize = Extract<ControlSize, "default" | "sm">;
@@ -18,9 +17,6 @@ const TabsFramedContext = React.createContext(false);
18
17
  type TabsOrientation = "horizontal" | "vertical";
19
18
  const TabsOrientationContext =
20
19
  React.createContext<TabsOrientation>("horizontal");
21
- const TabsListVariantContext = React.createContext<"default" | "line">(
22
- "default",
23
- );
24
20
 
25
21
  function Tabs({
26
22
  className,
@@ -62,87 +58,62 @@ function Tabs({
62
58
  );
63
59
  }
64
60
 
65
- const tabsListVariants = cva(
66
- // A altura horizontal entra como classe simples no componente (abaixo), para que um
67
- // `className="h-full"` do consumidor consiga substituí-la via tailwind-merge. Seletores
68
- // condicionais por data-size têm especificidade maior e tornavam a altura inextensível.
69
- "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[0.1875rem] text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none data-[variant=line]:p-0",
70
- {
71
- variants: {
72
- variant: {
73
- default: "bg-muted",
74
- line: "gap-1 bg-transparent",
75
- },
76
- },
77
- defaultVariants: {
78
- variant: "default",
79
- },
80
- },
81
- );
82
-
83
61
  function TabsList({
84
62
  className,
85
63
  children,
86
64
  style,
87
- variant = "default",
88
65
  ...props
89
- }: React.ComponentProps<typeof TabsPrimitive.List> &
90
- VariantProps<typeof tabsListVariants>) {
66
+ }: React.ComponentProps<typeof TabsPrimitive.List>) {
91
67
  // O size vem do <Tabs> (contexto) — a altura mora aqui, na list.
92
68
  const size = React.useContext(TabsSizeContext);
93
69
  const orientation = React.useContext(TabsOrientationContext);
94
70
  const distribution = React.useContext(TabsDistributionContext);
95
- const normalizedVariant = variant ?? "default";
96
- const hasHorizontalLine =
97
- normalizedVariant === "line" && orientation === "horizontal";
71
+ const hasHorizontalLine = orientation === "horizontal";
98
72
  return (
99
- <TabsListVariantContext.Provider value={normalizedVariant}>
100
- <TabsPrimitive.List
101
- data-slot="tabs-list"
102
- data-variant={normalizedVariant}
103
- data-size={size}
104
- data-distribution={distribution}
105
- className={cn(
106
- tabsListVariants({ variant: normalizedVariant }),
107
- controlHeight[size],
108
- distribution === "equal" && orientation === "horizontal" && "w-full",
109
- className,
110
- )}
111
- style={{
112
- ...(hasHorizontalLine
113
- ? {
114
- position: "relative",
115
- width: "100%",
116
- height: "auto",
117
- justifyContent: "flex-start",
118
- }
119
- : {}),
120
- ...style,
121
- }}
122
- {...props}
123
- >
124
- {hasHorizontalLine && (
125
- <span
126
- aria-hidden="true"
127
- data-slot="tabs-line"
128
- style={{
129
- position: "absolute",
130
- insetInline: "0rem",
131
- bottom: 0,
132
- borderBottomWidth: "0.0625rem",
133
- borderBottomStyle: "solid",
134
- borderBottomColor: "var(--border)",
135
- pointerEvents: "none",
136
- }}
137
- />
138
- )}
139
- {children}
140
- </TabsPrimitive.List>
141
- </TabsListVariantContext.Provider>
73
+ <TabsPrimitive.List
74
+ data-slot="tabs-list"
75
+ data-size={size}
76
+ data-distribution={distribution}
77
+ className={cn(
78
+ "group/tabs-list inline-flex w-fit items-center justify-center gap-1 rounded-none bg-transparent p-0 text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
79
+ controlHeight[size],
80
+ distribution === "equal" && orientation === "horizontal" && "w-full",
81
+ className,
82
+ )}
83
+ style={{
84
+ ...(hasHorizontalLine
85
+ ? {
86
+ position: "relative",
87
+ width: "100%",
88
+ height: "auto",
89
+ justifyContent: "flex-start",
90
+ }
91
+ : {}),
92
+ ...style,
93
+ }}
94
+ {...props}
95
+ >
96
+ {hasHorizontalLine && (
97
+ <span
98
+ aria-hidden="true"
99
+ data-slot="tabs-line"
100
+ style={{
101
+ position: "absolute",
102
+ insetInline: "0rem",
103
+ bottom: 0,
104
+ borderBottomWidth: "0.0625rem",
105
+ borderBottomStyle: "solid",
106
+ borderBottomColor: "var(--border)",
107
+ pointerEvents: "none",
108
+ }}
109
+ />
110
+ )}
111
+ {children}
112
+ </TabsPrimitive.List>
142
113
  );
143
114
  }
144
115
 
145
- function TabsTrigger({
116
+ function TabsListItem({
146
117
  className,
147
118
  style,
148
119
  icon,
@@ -152,17 +123,16 @@ function TabsTrigger({
152
123
  /** Ícone decorativo antes do rótulo. Em uma aba sem texto, declare também `aria-label`. */
153
124
  icon?: React.ReactNode;
154
125
  }) {
155
- const variant = React.useContext(TabsListVariantContext);
156
126
  const orientation = React.useContext(TabsOrientationContext);
157
127
  const size = React.useContext(TabsSizeContext);
158
128
  const distribution = React.useContext(TabsDistributionContext);
159
129
  const framed = React.useContext(TabsFramedContext);
160
- const hasHorizontalLine = variant === "line" && orientation === "horizontal";
130
+ const hasHorizontalLine = orientation === "horizontal";
161
131
  const content = (
162
132
  <>
163
133
  {icon !== undefined && (
164
134
  <span
165
- data-slot="tabs-trigger-icon"
135
+ data-slot="tabs-list-item-icon"
166
136
  aria-hidden="true"
167
137
  className="flex size-4 shrink-0 items-center justify-center [&_svg]:size-4"
168
138
  >
@@ -174,16 +144,14 @@ function TabsTrigger({
174
144
  );
175
145
  return (
176
146
  <TabsPrimitive.Trigger
177
- data-slot="tabs-trigger"
147
+ data-slot="tabs-list-item"
178
148
  // px: altura e indicador compensam a hairline do contêiner (um pixel físico, não escala).
179
149
  className={cn(
180
- "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
181
- "dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent",
182
- "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:text-foreground",
183
- "after:absolute after:bg-primary after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-1px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
150
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground dark:data-[state=active]:border-transparent dark:data-[state=active]:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
151
+ "after:absolute after:bg-primary after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-1px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 data-[state=active]:after:opacity-100",
184
152
  hasHorizontalLine &&
185
153
  cn(
186
- "group/tab-trigger rounded-none px-0 py-0 hover:bg-transparent active:brightness-100 data-[state=active]:bg-transparent data-[state=active]:hover:bg-transparent",
154
+ "group/tab-list-item rounded-none px-0 py-0 hover:bg-transparent active:brightness-100 data-[state=active]:bg-transparent data-[state=active]:hover:bg-transparent",
187
155
  framed ? "pb-2" : "pb-1.5",
188
156
  ),
189
157
  className,
@@ -202,9 +170,9 @@ function TabsTrigger({
202
170
  >
203
171
  {hasHorizontalLine ? (
204
172
  <span
205
- data-slot="tabs-trigger-content"
173
+ data-slot="tabs-list-item-content"
206
174
  className={cn(
207
- "inline-flex items-center justify-center gap-1.5 rounded-md px-3 transition-colors hover:bg-accent hover:text-accent-foreground active:brightness-90 group-data-[state=active]/tab-trigger:text-foreground",
175
+ "inline-flex items-center justify-center gap-1.5 rounded-md px-3 transition-colors hover:bg-accent hover:text-accent-foreground active:brightness-90 group-data-[state=active]/tab-list-item:text-foreground",
208
176
  distribution === "equal" && "w-full",
209
177
  controlHeight[size],
210
178
  )}
@@ -218,17 +186,17 @@ function TabsTrigger({
218
186
  );
219
187
  }
220
188
 
221
- function TabsContent({
189
+ function TabsPanel({
222
190
  className,
223
191
  ...props
224
192
  }: React.ComponentProps<typeof TabsPrimitive.Content>) {
225
193
  return (
226
194
  <TabsPrimitive.Content
227
- data-slot="tabs-content"
195
+ data-slot="tabs-panel"
228
196
  className={cn("flex-1 outline-none", className)}
229
197
  {...props}
230
198
  />
231
199
  );
232
200
  }
233
201
 
234
- export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
202
+ export { Tabs, TabsList, TabsListItem, TabsPanel };
@@ -15,7 +15,7 @@ const toggleVariants = cva(
15
15
  variant: {
16
16
  default: "bg-transparent",
17
17
  outline:
18
- "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
18
+ "border border-input bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground data-[state=on]:bg-muted/50 data-[state=on]:text-foreground",
19
19
  },
20
20
  // Alturas da escala de control.ts (`sm` = o mesmo h-8 de Button e Select).
21
21
  size: {
@@ -42,7 +42,6 @@ extremos opostos do cabeçalho da coleção; busca, filtros e atualização perm
42
42
  <Content
43
43
  title="Workspaces"
44
44
  level={1}
45
- variant="page"
46
45
  actions={<Button>Criar workspace</Button>}
47
46
  >
48
47
  <ActionList action={workspaceList} input={{}} />
@@ -238,7 +237,7 @@ render(
238
237
  <div className="h-80 w-full overflow-x-auto overflow-y-hidden rounded-lg border border-border">
239
238
  <Page layout="fill">
240
239
  <PageBody>
241
- <Content level={1} variant="page">
240
+ <Content level={1}>
242
241
  <ContentHeader>
243
242
  <ContentTitle>Workspaces</ContentTitle>
244
243
  <ContentActions>
@@ -276,7 +275,7 @@ componentes no caminho. Nos demais casos, a decisão é sua:
276
275
 
277
276
  - Para a lista manter a altura dos resultados, como em uma seção secundária cuja rolagem pertence ao
278
277
  `PageBody`, declare `layout="flow"`.
279
- - Um contêiner próprio entre `ContentBody` e a lista, como um `TabsContent`, não repassa a altura, e
278
+ - Um contêiner próprio entre `ContentBody` e a lista, como um `TabsPanel`, não repassa a altura, e
280
279
  a lista volta a `flow`. Declare `layout="fill"` na lista e dê ao contêiner as classes
281
280
  `flex min-h-0 flex-1 flex-col`.
282
281
  - Dentro de [`Card`](/ui/card), [`Dialog`](/ui/dialog), [`Drawer`](/ui/drawer) ou
@@ -364,6 +363,9 @@ render(
364
363
 
365
364
  - **`depends: ['outroFiltro']`:** desabilita o filtro até que suas dependências tenham valor e o
366
365
  limpa quando uma delas muda.
366
+ - **`presentation: 'toggle'`:** mantém visíveis as opções curtas de um `select` de escolha única;
367
+ estado, URL e input da action continuam iguais aos do controle padrão. Entradas de dicionário
368
+ com `icon` apresentam o ícone junto ao rótulo.
367
369
  - **`options: { kind: 'dictionary', ref: 'sessionStatus' }`:** resolve opções e rótulos por um
368
370
  dicionário registrado no `OpusProvider`. `filterOptions` substitui essa fonte quando informado.
369
371
  - **`options: { kind: 'lookup', source: 'x.lookup' }`:** consulta uma action com `{ q }` e os valores
@@ -371,6 +373,13 @@ render(
371
373
 
372
374
  ```tsx
373
375
  filters: {
376
+ channel: {
377
+ label: 'Canal',
378
+ type: 'select',
379
+ presentation: 'toggle',
380
+ allowEmpty: false,
381
+ options: { kind: 'static', items: [...] },
382
+ },
374
383
  project: { label: 'Projeto', type: 'select', options: { kind: 'static', items: [...] } },
375
384
  sprint: {
376
385
  label: 'Sprint',
@@ -469,7 +478,7 @@ render(
469
478
  | Chave | O que declara |
470
479
  | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
471
480
  | `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
472
- | `filters` | `{ [nome]: { label, type, options?, multiple?, searchable?, allowEmpty?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `searchable` habilita busca local em `select`; `allowEmpty: false` omite a opção vazia. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
481
+ | `filters` | `{ [nome]: { label, type, options?, multiple?, presentation?, searchable?, allowEmpty?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `presentation: 'toggle'` mostra todas as opções curtas de um `select` de escolha única em um `ToggleGroup`; sem a propriedade, o controle continua sendo `Select`. `searchable` habilita busca local em `select`; `allowEmpty: false` omite a opção vazia. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
473
482
  | `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Ela aparece depois dos filtros inline e imediatamente antes do acesso aos filtros avançados. A busca participa da contagem de controles inline: com mais de um controle, a barra exibe as labels; quando ela é o único recorte, sua label é omitida. |
474
483
  | `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
475
484
  | `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
@@ -120,8 +120,8 @@ nome de uma action não começa pelo caminho do domínio, como `workspace.` ou `
120
120
  `subdomains` aceita outros domínios e forma esse caminho.
121
121
 
122
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.
123
+ registrar só o contrato, sem handler: o plugin do modo design liga uma resposta gerada ou um
124
+ cenário; fora dele, a execução falha por falta de handler.
125
125
 
126
126
  ## Ordem canônica dos campos
127
127
 
@@ -132,9 +132,9 @@ nessa estrutura. Os grupos seguem esta sequência; dentro de cada grupo, a ordem
132
132
  ```text
133
133
  name, kind
134
134
  → label, title, summary, description, icon, color, messages, tags, examples, errors
135
- → input, output
135
+ → input, output, effect, personalData
136
136
  → public, requires, authorize, loads
137
- → handler, mockHandler
137
+ → handler
138
138
  → audit, confirm, invalidates, rateLimit, idempotency
139
139
  → ai, automatable, internal
140
140
  ```
@@ -146,6 +146,35 @@ Declare `input` e `output` com `z.object` e use os tipos lógicos do catálogo e
146
146
  validações: `z.object({ tag: t.slug().zod(), contato: t.email().zod() })`. O `.zod()` devolve o
147
147
  schema zod de cada tipo lógico (slug, email, phone, money…), importado de `@softize/opus/schema/zod`.
148
148
 
149
+ ## Declarar o que a action faz com os dados
150
+
151
+ Dois fatos sobre a action valem para qualquer consumidor, da interface à IA, e ficam declarados no
152
+ contrato:
153
+
154
+ - **`effect`** diz se a action só lê (`'read'`) ou altera estado (`'write'`). `list` e `view` já são
155
+ leitura, porque são servidas por `GET`, e não precisam declarar. `simple` e `form` declaram,
156
+ porque o `kind` não diz isso: uma análise sob demanda costuma ser `simple` e só ler.
157
+ - **`personalData`** diz se a resposta contém dados que identificam pessoas, como nome, documento,
158
+ contato, placa ou texto livre sobre alguém. `false` também é uma declaração.
159
+
160
+ ```ts
161
+ export const salesPerformanceContract = defineContract({
162
+ name: 'sales.performance',
163
+ kind: 'simple',
164
+ input: z.object({ period: z.string() }),
165
+ output: z.object({ total: z.number() }),
166
+ effect: 'read',
167
+ personalData: false,
168
+ })
169
+ ```
170
+
171
+ Com `personalData: true`, toda execução bem-sucedida fica marcada no registro de auditoria com a
172
+ quantidade de registros devolvidos, inclusive quando a action declara `audit: false`; nesse caso o
173
+ registro é mínimo, sem entrada nem saída. No modo design, a resposta simulada não é marcada. Um
174
+ host de IA pode usar `effect` para decidir o que oferecer ao modelo sem procedimento, e o MCP publica
175
+ a anotação `readOnlyHint`. O `opus check` avisa quantas actions ainda não declaram os dois fatos; uma
176
+ major futura passará a exigi-los.
177
+
149
178
  A autorização é fechada por padrão: sem `public: true`, a action exige usuário autenticado; com
150
179
  `authorize`, exige também a permissão declarada. `automatable: true` permite que um
151
180
  [schedule](/docs/scheduler) execute a action como o sistema, sem abri-la a chamadores anônimos.
@@ -199,10 +228,9 @@ O Opus não decide onde os cenários ficam nem se entram no repositório. Eles n
199
228
  aplicação: podem ficar numa pasta ignorada pelo git e ser escritos durante a sessão de design,
200
229
  quando forem necessários.
201
230
 
202
- `mockHandler` está obsoleto desde a 22.1.0 e sai na próxima major. Enquanto existir, ele ainda
203
- responde no modo design, abaixo do cenário e acima da resposta gerada, e o `opus check` conta
204
- quantas actions ainda o declaram. Para migrar, apague o `mockHandler`; se a resposta dele importar
205
- para o design, transforme-a em cenário.
231
+ Dados de design não pertencem ao contrato. Quando uma resposta específica importar para a revisão
232
+ da interface, declare um cenário com `defineDesignScenario`; para os demais casos, preserve apenas
233
+ o contrato e deixe o plugin gerar a resposta pelo schema.
206
234
 
207
235
  O plugin `opusDesign()`, de `@softize/opus/vite`, monta o runtime do Opus dentro do dev server e
208
236
  responde `/api` no próprio processo. No modo design, qualquer `server.proxy` para um backend externo
@@ -205,15 +205,15 @@ Quando a região principal da página é uma coleção, `Content` nomeia e gover
205
205
  toolbar da lista. Essa divisão aproxima cada comando do objeto que ele afeta sem criar uma família
206
206
  paralela de componentes `List*`.
207
207
 
208
- Quando a coleção é o assunto da página, use `variant="page"` com `level={1}`: o `Content` assume a
209
- escala do cabeçalho de [`Page`](/ui/page) e passa a ser o heading principal. As ações ficam no
210
- extremo oposto ao título, como em qualquer variante.
208
+ Quando a coleção é o assunto da página, use `level={1}`: o `Content` passa a ser o heading
209
+ principal, assume automaticamente a escala do cabeçalho de [`Page`](/ui/page) e mantém o mesmo
210
+ ritmo entre introdução e corpo. As ações ficam no extremo oposto ao título. Níveis de 2 a 6
211
+ continuam usando a apresentação compacta de uma seção.
211
212
 
212
213
  ```tsx preview col
213
214
  render(
214
215
  <Content
215
216
  level={1}
216
- variant="page"
217
217
  title="Clientes"
218
218
  actions={<Button>Criar cliente</Button>}
219
219
  >
@@ -242,8 +242,7 @@ classes adicionais; um `Content` irmão, sem lista, mantém a altura do próprio
242
242
  | `count` | `number` | | Forma curta: total de itens ao lado do título da região. |
243
243
  | `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. |
244
244
  | `actions` | `ReactNode` | | Forma curta: ações sobre a região inteira, no extremo oposto ao título. |
245
- | `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível semântico do heading, independente do destaque visual. |
246
- | `variant` | `'page' \| 'section'` | `'section'` | Hierarquia visual: `page` reproduz o cabeçalho de `Page`; `section` é a região dentro de uma superfície. |
245
+ | `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível do heading; `1` identifica o conteúdo principal da página e os demais, uma seção interna. |
247
246
  | `as` | `'section' \| 'fieldset'` | `'section'` | Usa uma região com heading ou um grupo de campos cujo título é a legenda nativa. |
248
247
  | `framed` | `boolean` | `false` | Envolve o header e o body com a moldura estrutural da região. |
249
248
  | `layout` | `'continuous' \| 'sectioned'` | `'continuous'` | Mantém header e body contínuos ou os separa com divisor e body sem recuo. `sectioned` exige `framed`. |
@@ -56,6 +56,12 @@ entrada. Em `_meta`, cada tool traz os
56
56
  [Produtos de Dados](/docs/data-products) relacionados: `com.softize.opus/data-products` lista os
57
57
  identificadores e `com.softize.opus/data-product-labels`, o nome legível de cada um.
58
58
 
59
+ Quando a action declara o que faz com os dados, a tool traz também as anotações padrão do MCP:
60
+ `readOnlyHint` vem de `effect`. Numa action que escreve, `destructiveHint` vai sempre explícito, a
61
+ partir de `ai.destructive`, porque o MCP trata a ausência numa escrita como destrutiva.
62
+ Em `_meta`, `com.softize.opus/effect` repete o efeito e `com.softize.opus/personal-data` diz se a
63
+ resposta contém dado pessoal. Veja [Actions](/docs/actions).
64
+
59
65
  `CallTool` recusa com erro qualquer nome fora dessa lista; os demais seguem por `runtime.execute`.
60
66
  A exposição declarada em `ai` decide o que o cliente MCP pode executar, e a autorização do contexto
61
67
  continua valendo sobre ela. Para um acesso somente de leitura, exponha apenas actions de leitura.
@@ -54,7 +54,6 @@ O par `context` + `variant` vale para componentes que comunicam significado, com
54
54
  | --- | --- | --- |
55
55
  | [`Select`](/ui/select) | `default`, `outline`, `ghost` | `default` é o campo completo; `outline` é compacto com borda, e `ghost`, compacto sem borda nem fundo. |
56
56
  | [`Toggle`](/ui/toggle) | `default`, `outline` | `default` não tem borda nem fundo em repouso; o fundo aparece no hover e quando ativo. `outline` acrescenta a borda. |
57
- | [`TabsList`](/ui/tabs) | `default`, `line` | `default` agrupa as abas em uma superfície preenchida; `line` usa uma linha-base e marca nela a aba ativa. |
58
57
 
59
58
  O mesmo nome pode aparecer nos dois grupos. Num `Badge`, `outline` é um tratamento do significado
60
59
  declarado em `context`; num `Select`, é apenas o formato compacto com borda.