@softize/opus 25.0.0 → 25.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/assistant-schema.d.mts +55 -0
- package/bin/lib/assistant-schema.mjs +286 -0
- package/bin/lib/check.mjs +173 -0
- package/bin/lib/copy.mjs +2 -0
- package/bin/lib/gen-manifest.mjs +3 -0
- package/bin/lib/gen-runner.mjs +29 -1
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0052-floating-layer-coexists-with-modal-surfaces.md +668 -0
- package/docs/adr/0053-read-action-declares-the-resource-assistant.md +443 -0
- package/docs/protocol.md +54 -0
- package/docs/releasing.md +8 -2
- package/docs/stacking-scale.md +53 -0
- package/package.json +10 -1
- package/registry/skills/create-opus-action/SKILL.md +8 -3
- package/registry/skills/create-opus-action/references/contract-and-binding.md +24 -1
- package/registry/skills/create-opus-action/references/evaluations.md +3 -0
- package/registry/skills/test-opus-action/SKILL.md +4 -1
- package/registry/skills/test-opus-action/references/evaluations.md +2 -0
- package/registry/skills/test-opus-action/references/harness.md +14 -0
- package/src/ai/index.ts +3 -0
- package/src/ai/resource-context.ts +113 -0
- package/src/core/assistant.ts +545 -0
- package/src/core/index.ts +23 -0
- package/src/core/runtime.ts +136 -0
- package/src/core/types.ts +34 -0
- package/src/mcp/index.ts +3 -0
- package/src/ui/components/primitives/dialog.tsx +285 -26
- package/src/ui/components/primitives/drawer.tsx +104 -13
- package/src/ui/components/primitives/floating-layer.tsx +79 -0
- package/src/ui/components/primitives/menu.tsx +17 -2
- package/src/ui/components/primitives/popover.tsx +6 -1
- package/src/ui/components/primitives/tooltip.tsx +6 -1
- package/src/ui/docs/content/actions.md +97 -1
- package/src/ui/docs/content/ai.md +4 -2
- package/src/ui/docs/content/cli.md +2 -1
- package/src/ui/docs/content/dialog.md +3 -0
- package/src/ui/docs/content/drawer.md +3 -0
- package/src/ui/docs/content/floating-layer.md +178 -0
- package/src/ui/docs/content/mcp.md +3 -1
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/lib/dismiss-on-iframe.ts +4 -1
- package/src/ui/lib/modal-layers.ts +547 -0
- package/src/ui/meta.ts +6 -0
- package/src/ui/react.tsx +4 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# ADR 0053 — A action de leitura declara o assistente do recurso
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-03.
|
|
5
|
+
- **Complementa:** ADR 0041 (efeito e dado pessoal no contrato) e ADR 0013 (Presentation como
|
|
6
|
+
artefato portátil orientado a action).
|
|
7
|
+
- **Relacionadas:** ADR 0111 da Grand Brasil, primeiro consumidor desta decisão; ADRs 0004, 0069,
|
|
8
|
+
0087, 0103 e 0104 da Grand Brasil.
|
|
9
|
+
|
|
10
|
+
## Contexto
|
|
11
|
+
|
|
12
|
+
Um produto feito com o Opus pode oferecer, além do chat geral, um chat especializado num recurso: a
|
|
13
|
+
conversa sobre um cliente, um relatório ou um fluxo. Esse chat precisa saber três coisas: como ler
|
|
14
|
+
o recurso e conferir se a pessoa ainda pode vê-lo, qual procedimento a IA segue ao tratar dele e
|
|
15
|
+
quais dados do recurso entram na conversa. Hoje nenhuma dessas três coisas está no contrato.
|
|
16
|
+
|
|
17
|
+
A Grand Brasil mostra o custo de deixá-las fora. Cada uma mora num lugar diferente, escrita à mão:
|
|
18
|
+
|
|
19
|
+
- O procedimento e a persona de cada tipo de recurso estão numa tabela no motor de IA
|
|
20
|
+
(`services/ai/src/resource-assistant.ts:17-61`).
|
|
21
|
+
- O dado que vai ao chat é montado na interface, tela por tela
|
|
22
|
+
(`apps/main/src/copilot/context.ts:89-139`).
|
|
23
|
+
- A conferência de acesso é uma rota com um `if` por tipo
|
|
24
|
+
(`services/main/src/server/main.ts:207-262`). Para relatórios, são duas rotas que chamam a regra
|
|
25
|
+
de acesso do domínio direto, sem passar pela action `report.read`
|
|
26
|
+
(`services/main/src/server/report-access-route.ts:22-37` e
|
|
27
|
+
`services/main/src/server/report-routes.ts:28-44`). A leitura do relatório por essas rotas não
|
|
28
|
+
gera registro de auditoria nem aparece na Lente.
|
|
29
|
+
|
|
30
|
+
O que o Opus já projeta não basta para mostrar esse desenho:
|
|
31
|
+
|
|
32
|
+
- O manifest projeta `effect` e `personalData`, mas não a configuração `ai`
|
|
33
|
+
(`bin/lib/gen-manifest.mjs:39-68`). Quem lê o manifest não sabe nem se a action está publicada
|
|
34
|
+
para a IA.
|
|
35
|
+
- A Lente (`@softize/lens`) lê o manifest e, por action, mostra nome, tipo, documentação,
|
|
36
|
+
permissão, eventos, entrada e saída (`lens/src/structure.ts:318-333`). Não mostra efeito, dado
|
|
37
|
+
pessoal, publicação para a IA nem assistente.
|
|
38
|
+
- `runtime.aiTools()` (`src/core/runtime.ts:997-1037`) e o `ListTools` do MCP
|
|
39
|
+
(`src/mcp/index.ts:54-60`) carregam efeito, dado pessoal e Produtos de Dados. O motor de IA da
|
|
40
|
+
Grand Brasil já lê esses metadados pelo MCP (`services/ai/src/mcp.ts:93-101`). É um canal pronto
|
|
41
|
+
para levar a declaração ao host.
|
|
42
|
+
- A Presentation já declara `assistant: { triggerLabel }` (`src/core/presentation.ts:125-130` e
|
|
43
|
+
`160`). Essa declaração cobre só a interface: o botão e a divisão da superfície. Não diz o que o
|
|
44
|
+
chat faz.
|
|
45
|
+
|
|
46
|
+
As forças em tensão:
|
|
47
|
+
|
|
48
|
+
- **Um lugar só.** Leitura, conferência de acesso, procedimento e dado ao modelo precisam vir da
|
|
49
|
+
mesma declaração, para não divergirem.
|
|
50
|
+
- **Contrato compartilhável.** O contrato roda no cliente e no servidor. A declaração não pode
|
|
51
|
+
carregar função de servidor, banco nem segredo, e precisa caber no manifest.
|
|
52
|
+
- **O procedimento é dado do consumidor.** Na Grand Brasil, Habilidades são registros
|
|
53
|
+
administráveis no banco do serviço de IA. O Opus não as conhece, e o `opus check`, que é
|
|
54
|
+
estático, não as alcança.
|
|
55
|
+
- **O dado ao modelo é mínimo, limitado e tratado como dado.** A Grand Brasil já decidiu que texto
|
|
56
|
+
vindo de fora entra no turno como bloco delimitado, nunca como instrução (ADR 0004 dela, seção
|
|
57
|
+
3), e pôs teto no que entra no turno (seção 2 da mesma ADR). Uma projeção sem teto repetiria o
|
|
58
|
+
problema de custo e de diluição do prompt que motivou aquela decisão.
|
|
59
|
+
- **Dado pessoal.** O dado do recurso pode identificar pessoas. A marca de exposição já é do
|
|
60
|
+
runtime (ADR 0041) e precisa continuar valendo.
|
|
61
|
+
- **Fronteira do SDK.** Persona, regras de negócio e o que cada tipo de recurso significa são do
|
|
62
|
+
consumidor.
|
|
63
|
+
|
|
64
|
+
## Decisão
|
|
65
|
+
|
|
66
|
+
### 1. Uma action `view` pode declarar o assistente do recurso que ela lê
|
|
67
|
+
|
|
68
|
+
O contrato ganha o campo opcional `assistant`. A action que o declara passa a ser a **action dona**
|
|
69
|
+
daquele tipo de recurso: é por ela que o recurso é lido e que o acesso a ele é conferido.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export const customerViewContract = defineContract({
|
|
73
|
+
name: 'customer.view',
|
|
74
|
+
kind: 'view',
|
|
75
|
+
label: 'Ver cliente',
|
|
76
|
+
description: 'Reúne o cadastro canônico, a qualidade dos contatos e o relacionamento comercial.',
|
|
77
|
+
input: z.object({ id: z.string().uuid() }),
|
|
78
|
+
output: customerViewSchema,
|
|
79
|
+
personalData: true,
|
|
80
|
+
requires: commercialRead,
|
|
81
|
+
authorize: anyPerm(...commercialRead),
|
|
82
|
+
ai: true,
|
|
83
|
+
assistant: {
|
|
84
|
+
resource: 'customer',
|
|
85
|
+
skill: 'review-customer',
|
|
86
|
+
label: 'Assistente do cliente',
|
|
87
|
+
title: 'name',
|
|
88
|
+
context: ['kind', 'stage', 'sources', 'leads.totalCount', 'leads.latest.status'],
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
O exemplo deixa fora do contexto contato, documento, data de nascimento e endereço. O nome já vai
|
|
94
|
+
como `title` e não se repete em `context`.
|
|
95
|
+
|
|
96
|
+
| Campo | O que declara |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `resource` | O tipo de recurso de que a action é dona. Identificador em camelCase, único no runtime. |
|
|
99
|
+
| `skill` | A chave do procedimento que a IA segue ao tratar desse recurso, no registro do consumidor. |
|
|
100
|
+
| `label` | O nome do assistente para pessoas. Entra no inventário do `opus copy`. |
|
|
101
|
+
| `title` | O caminho, na saída, do campo que nomeia o recurso para pessoas. |
|
|
102
|
+
| `context` | Os caminhos, na saída, dos campos que o host pode entregar ao modelo como dado do recurso. |
|
|
103
|
+
| `maxContextChars` | Opcional. Teto do dado serializado: padrão de 4.000 caracteres, mínimo de 1.000 e máximo de 16.000. |
|
|
104
|
+
|
|
105
|
+
Regras da declaração:
|
|
106
|
+
|
|
107
|
+
- Só uma action `view` declara assistente. A leitura de um recurso identificado é o papel da `view`.
|
|
108
|
+
- A action precisa estar publicada para a IA (`ai` habilitado). Um assistente cujo recurso a IA
|
|
109
|
+
não pode ler seria uma contradição.
|
|
110
|
+
- A action precisa declarar `personalData`, `true` ou `false`. Com assistente, a declaração deixa
|
|
111
|
+
de ser opcional, porque decide como o host trata o dado que entrega ao modelo.
|
|
112
|
+
- A entrada é um objeto em que `id`, do tipo texto, é o único campo obrigatório; outros campos, se
|
|
113
|
+
houver, têm valor padrão. É só com `{ id }` que o runtime pede o recurso.
|
|
114
|
+
- Os caminhos usam ponto para descer em objetos (`leads.totalCount`). Um caminho que passa por uma
|
|
115
|
+
lista se aplica a cada item. Um caminho pode atravessar um campo anulável, como `leads` em
|
|
116
|
+
`customer.view`: quando o valor intermediário é `null`, a projeção entrega `null` naquele ponto.
|
|
117
|
+
- Cada `resource` tem uma única action dona no runtime.
|
|
118
|
+
|
|
119
|
+
`assistant` fica no grupo de integração da ordem canônica, o mesmo de `ai`. O `opus check` confere
|
|
120
|
+
o grupo, não a posição dentro dele (`bin/lib/check.mjs:48-77`).
|
|
121
|
+
|
|
122
|
+
### 2. O runtime lê o recurso pela action dona
|
|
123
|
+
|
|
124
|
+
O runtime ganha duas leituras:
|
|
125
|
+
|
|
126
|
+
- **`runtime.assistants()`** devolve a lista das declarações registradas, com a action dona de
|
|
127
|
+
cada uma. Serve ao consumidor para montar rotas e telas a partir da declaração, sem lista
|
|
128
|
+
paralela, e para conferir as chaves de procedimento onde também tiver o registro (seção 5).
|
|
129
|
+
- **`runtime.readResource(resource, id, base)`** executa a action dona com `{ id }` pelo mesmo
|
|
130
|
+
`execute` de qualquer chamada. A autorização, a auditoria, a marca de dado pessoal e o span são
|
|
131
|
+
os da action. Em caso de sucesso, devolve:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
{
|
|
135
|
+
ok: true,
|
|
136
|
+
resource: { kind: 'customer', id, title: 'Maria Souza', label: 'Assistente do cliente', skill: 'review-customer' },
|
|
137
|
+
context: { data: { ... }, truncated: false, omitted: [], personalData: true },
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Em caso de falha, devolve o erro da action, com a categoria dela. Um tipo de recurso sem action
|
|
142
|
+
dona devolve `runtime.assistant_resource_unknown`, sem executar nada.
|
|
143
|
+
|
|
144
|
+
Não há outro caminho no SDK para ler o recurso de um assistente. A declaração não exporta regra de
|
|
145
|
+
acesso, consulta nem atalho que dispense a action.
|
|
146
|
+
|
|
147
|
+
### 3. O contexto é uma projeção limitada, entregue como dado
|
|
148
|
+
|
|
149
|
+
`context.data` contém somente os caminhos declarados, com a mesma estrutura da saída. O que não foi
|
|
150
|
+
declarado não sai.
|
|
151
|
+
|
|
152
|
+
Quando o JSON serializado passa de `maxContextChars`, o runtime reduz a projeção por etapas, até
|
|
153
|
+
caber, e nunca em silêncio:
|
|
154
|
+
|
|
155
|
+
1. Remove o último item da lista que tem mais itens, e repete. Registra em `omitted` o caminho e
|
|
156
|
+
quantos itens saíram.
|
|
157
|
+
2. Com as listas vazias, encurta pela metade o texto mais longo, até o mínimo de 200 caracteres, e
|
|
158
|
+
repete. Registra o caminho em `omitted`.
|
|
159
|
+
3. Se ainda não couber, remove campos inteiros, do último declarado em `context` para o primeiro,
|
|
160
|
+
e registra cada um em `omitted`.
|
|
161
|
+
4. Marca `truncated: true`.
|
|
162
|
+
|
|
163
|
+
O teto conta só os dados de `context`. O tipo, o id e o título vão no bloco fora dessa conta, e o
|
|
164
|
+
título é sempre encurtado a no máximo 200 caracteres, com `…` e registro em `omitted`. Cada etapa
|
|
165
|
+
remove alguma coisa, e a terceira chega, no limite, a um objeto vazio, que cabe em qualquer teto
|
|
166
|
+
válido. Por isso o corte sempre termina.
|
|
167
|
+
|
|
168
|
+
Um caminho declarado pode faltar na saída em execução, porque o runtime só valida a saída em
|
|
169
|
+
desenvolvimento (`src/core/runtime.ts:907-912`). Nesse caso, a projeção omite o caminho, registra-o
|
|
170
|
+
em `omitted` com o motivo `missing`, emite um aviso no log e não falha a leitura.
|
|
171
|
+
|
|
172
|
+
A projeção é uma função pura do core, `projectAssistantContext(assistant, output)`, usada por
|
|
173
|
+
`readResource` e exportada. Um host que recebe a saída da action por outro canal, como o MCP,
|
|
174
|
+
aplica a mesma projeção sem reimplementá-la.
|
|
175
|
+
|
|
176
|
+
O host entrega a projeção ao modelo como bloco de dado delimitado, numa mensagem de dado, e nunca
|
|
177
|
+
no prompt de sistema. O bloco leva o tipo, o id, o título, os dados e, quando houver corte,
|
|
178
|
+
`truncated` e `omitted`: o modelo precisa saber que o dado está incompleto para decidir chamar a
|
|
179
|
+
action. Para hosts que usam o driver de IA do Opus, o pacote oferece a função que monta o bloco e
|
|
180
|
+
neutraliza o delimitador dentro dos valores. O host pode usar o próprio formato, desde que cumpra a
|
|
181
|
+
mesma regra.
|
|
182
|
+
|
|
183
|
+
A projeção governa só o que o host entrega **por iniciativa própria**, como o dado do recurso que
|
|
184
|
+
acompanha um turno de conversa vinculada. Quando e com que frequência entregar é decisão do host.
|
|
185
|
+
A recomendação do Opus é reaproveitar a execução que já confere o acesso, em vez de executar a
|
|
186
|
+
action de novo só para obter o contexto: cada execução de uma action com dado pessoal é uma marca
|
|
187
|
+
de exposição. Quando o modelo chama a action dona como ferramenta, recebe a saída que o
|
|
188
|
+
contrato declara, como em qualquer chamada. Filtrar a resposta pelo consumidor faria a mesma action
|
|
189
|
+
devolver coisas diferentes para a tela e para a IA. Um campo que não deve chegar ao modelo em
|
|
190
|
+
nenhuma hipótese pede outra decisão no contrato, e não uma projeção.
|
|
191
|
+
|
|
192
|
+
**Dado pessoal.** A execução da action dona é marcada no registro da requisição como qualquer
|
|
193
|
+
outra (ADR 0041). `context.personalData` repete a declaração da action para que o host marque a
|
|
194
|
+
conversa: o Opus não conhece conversas.
|
|
195
|
+
|
|
196
|
+
### 4. Projeções: manifest, ferramentas, MCP e Lente
|
|
197
|
+
|
|
198
|
+
- **Manifest.** Cada action passa a projetar `ai` normalizado (`enabled`, `description`,
|
|
199
|
+
`destructive`, `requiresConfirmation`) e, quando houver, `assistant` com todos os campos. A
|
|
200
|
+
publicação para a IA entra no manifest junto, porque sem ela a Lente não consegue dizer o que a
|
|
201
|
+
IA alcança.
|
|
202
|
+
- **`runtime.aiTools()`.** A ferramenta da action dona leva `metadata.assistant` com `resource`,
|
|
203
|
+
`skill`, `label`, `title`, `context` e `maxContextChars`. *Nota (2026-10-03):* o teto, já
|
|
204
|
+
resolvido, entrou na implementação para o host que projeta pelo MCP usar o mesmo valor; ver a
|
|
205
|
+
nota de implementação na seção 10.
|
|
206
|
+
- **MCP.** O `ListTools` acrescenta `com.softize.opus/assistant` em `_meta`, com o mesmo conteúdo.
|
|
207
|
+
O host de IA conhece o assistente pelo catálogo que já consulta, sem configuração própria.
|
|
208
|
+
- **Lente.** A estrutura de cada action passa a carregar `ai`, `effect`, `personalData` e
|
|
209
|
+
`assistant`. A página da action mostra se ela está publicada para a IA, se é dona de um tipo de
|
|
210
|
+
recurso, que procedimento o assistente segue e quais campos vão ao modelo. A mudança é feita no
|
|
211
|
+
repositório da Lente, com versão própria, lendo o manifest como hoje.
|
|
212
|
+
|
|
213
|
+
### 5. O procedimento é referência por chave, conferida pelo consumidor
|
|
214
|
+
|
|
215
|
+
`skill` é uma chave opaca para o Opus, em minúsculas com hífen (`review-customer`). O `opus check`
|
|
216
|
+
confere a forma e a presença; não confere se o procedimento existe.
|
|
217
|
+
|
|
218
|
+
Quem confere é o consumidor, onde tiver acesso às duas fontes: num teste que compare as
|
|
219
|
+
declarações (o manifest versionado ou `runtime.assistants()`) com os procedimentos que o consumidor
|
|
220
|
+
versiona, ou no turno, quando o host já tem o catálogo e o registro em mãos. A subida de um serviço
|
|
221
|
+
não é um bom lugar: as duas fontes podem estar em serviços diferentes, e o catálogo pode exigir
|
|
222
|
+
credencial. Na Grand Brasil, as declarações vivem no serviço principal, as Habilidades no serviço
|
|
223
|
+
de IA, e o MCP interno só lista ferramentas com sessão e credencial de turno
|
|
224
|
+
(`packages/agent-authority/src/staff-mcp.ts:95-109` dela).
|
|
225
|
+
|
|
226
|
+
A Lente mostra a chave declarada. Mostrar se a chave resolve exige que o host informe esse estado
|
|
227
|
+
à Lente, o que fica fora desta decisão.
|
|
228
|
+
|
|
229
|
+
### 6. Acesso a recurso só por action
|
|
230
|
+
|
|
231
|
+
Para o Opus, o princípio é: o recurso de um assistente é lido, e o acesso a ele é conferido,
|
|
232
|
+
somente pela action dona. O SDK cumpre a sua parte nas seções 2 e 3.
|
|
233
|
+
|
|
234
|
+
O Opus não enxerga as rotas HTTP que o consumidor registra por conta própria. O teste que impede
|
|
235
|
+
uma rota de conferir acesso sem action é do consumidor. O `docs/protocol.md` registra o princípio e
|
|
236
|
+
recomenda esse teste.
|
|
237
|
+
|
|
238
|
+
### 7. O que o `opus check`, o `opus gen` e o `opus copy` cobram
|
|
239
|
+
|
|
240
|
+
A regra `assistant-declaration` do `opus check` reprova:
|
|
241
|
+
|
|
242
|
+
- `assistant` em action que não é `view`;
|
|
243
|
+
- `assistant` com `ai: false` ou sem `ai`;
|
|
244
|
+
- `assistant` sem `personalData` declarado;
|
|
245
|
+
- `resource` fora de camelCase ou `skill` fora de minúsculas com hífen;
|
|
246
|
+
- `label`, `title` ou `context` ausentes;
|
|
247
|
+
- o mesmo `resource` em dois contratos, pelo valor literal, entre arquivos.
|
|
248
|
+
|
|
249
|
+
O `opus gen`, que já converte a saída em JSON Schema (`bin/lib/gen-runner.mjs:399`), recusa gerar
|
|
250
|
+
o manifest quando `title` ou um caminho de `context` não existem na saída, ou quando a entrada não
|
|
251
|
+
tem `id` de texto como único campo obrigatório. Num campo anulável ou numa união (`anyOf` no JSON
|
|
252
|
+
Schema), o caminho vale se existir em algum ramo que não seja `null`.
|
|
253
|
+
|
|
254
|
+
O `register` do runtime recusa duas actions donas do mesmo `resource`, com
|
|
255
|
+
`runtime.assistant_duplicate_resource`, e um `maxContextChars` fora do intervalo, com
|
|
256
|
+
`runtime.assistant_context_limit_invalid`. Os erros desta decisão usam o prefixo
|
|
257
|
+
`runtime.assistant_`.
|
|
258
|
+
|
|
259
|
+
O `opus copy` passa a ler `assistant.label`.
|
|
260
|
+
|
|
261
|
+
### 8. Relação com a Presentation
|
|
262
|
+
|
|
263
|
+
`assistant.triggerLabel` da Presentation continua sendo a capacidade de interface: o botão no
|
|
264
|
+
header e a divisão da superfície. A declaração da action é o comportamento do chat. As duas são
|
|
265
|
+
independentes nesta decisão: o assistente existe também onde não há Presentation, como num drawer
|
|
266
|
+
composto à mão ou no chat geral.
|
|
267
|
+
|
|
268
|
+
### 9. O que fica fora
|
|
269
|
+
|
|
270
|
+
- Endereço de navegação do recurso. É da interface do consumidor.
|
|
271
|
+
- Persona, regras próprias de cada tipo e a forma de ativar o procedimento no turno. São do host.
|
|
272
|
+
- Escrita. A declaração descreve a leitura; a escrita continua governada por `effect`, `ai` e pelo
|
|
273
|
+
host.
|
|
274
|
+
|
|
275
|
+
### 10. As questões que ficaram para decisão
|
|
276
|
+
|
|
277
|
+
A proposta deixou cinco questões para decidir antes da implementação. Em 2026-10-03, todas foram
|
|
278
|
+
decididas como a proposta recomendava; cada item registra a questão, a decisão e o motivo.
|
|
279
|
+
|
|
280
|
+
1. **Nomes finais.** *Decisão:* `assistant`, `resource`, `skill`, `label`, `title`, `context` e
|
|
281
|
+
`runtime.readResource`. `skill` acompanha o identificador técnico que os consumidores já usam;
|
|
282
|
+
para pessoas, a Grand Brasil continua dizendo Habilidade.
|
|
283
|
+
2. **Teto do contexto.** *Decisão:* 4.000 caracteres por padrão, 1.000 no mínimo e 16.000 no
|
|
284
|
+
máximo (seções 1 e 3). Cabe o resumo de um cadastro e fica longe do teto de histórico que a
|
|
285
|
+
Grand Brasil usa (40.000).
|
|
286
|
+
3. **Projeção nas respostas de ferramenta.** *Decisão:* não aplicar, pelos motivos da seção 3.
|
|
287
|
+
4. **Rótulo do botão da Presentation.** *Decisão:* `triggerLabel` e `assistant.label` ficam
|
|
288
|
+
independentes (seção 8). Herdar o rótulo da action do corpo na Presentation, caso a repetição
|
|
289
|
+
apareça na prática, pede uma decisão própria.
|
|
290
|
+
5. **Versão.** *Decisão:* minor, porque o campo é opcional e a regra só cobra quem declara.
|
|
291
|
+
|
|
292
|
+
**Nota de implementação (2026-10-03).** A implementação preencheu os pontos que a decisão deixava em
|
|
293
|
+
aberto assim:
|
|
294
|
+
|
|
295
|
+
- **Projeção sob pedido.** `runtime.readResource` ganhou um quarto parâmetro opcional,
|
|
296
|
+
`{ context?: boolean }`. Sem ele, a leitura devolve só o descritor, que é o que a listagem de um
|
|
297
|
+
histórico usa, e resolve apenas o título (`resolveAssistantTitle`), sem serializar nem cortar a
|
|
298
|
+
saída; com `context: true`, devolve também a projeção. A execução da action e a autorização são as
|
|
299
|
+
mesmas nos dois casos. O aviso de título ausente vale sempre; o de caminho de `context` ausente,
|
|
300
|
+
só quando a projeção roda.
|
|
301
|
+
- **Falha na projeção.** Uma saída que o JSON não representa (`BigInt`, referência circular) não
|
|
302
|
+
derruba a leitura com exceção: `readResource` registra o erro no log e devolve
|
|
303
|
+
`runtime.assistant_projection_failed` (categoria `internal`). A execução da action já foi
|
|
304
|
+
auditada.
|
|
305
|
+
- **Tipo sem dona.** `runtime.assistant_resource_unknown` tem mensagem fixa; o tipo recebido vai em
|
|
306
|
+
`meta.resource`, para não ecoar entrada de fora no texto do erro.
|
|
307
|
+
- **Registro.** Além do dono duplicado e do teto, o `register` recusa a forma inválida da
|
|
308
|
+
declaração com `runtime.assistant_declaration_invalid`: `assistant` fora de `view`, `ai` não
|
|
309
|
+
habilitado, `personalData` não declarado, `resource` ou `skill` fora do formato, `label`, `title`
|
|
310
|
+
ou `context` ausentes ou caminho com segmento vazio. Quem não roda o `opus check` no gate recebe a
|
|
311
|
+
mesma régua ao subir o serviço. A entrada `{ id }` continua conferida só pelo `opus gen`, que
|
|
312
|
+
enxerga o JSON Schema; o core segue sem dependência de schema.
|
|
313
|
+
- **Teto nas projeções.** `aiTools()`, o `_meta` do MCP, o manifest e `runtime.assistants()` levam
|
|
314
|
+
a mesma forma, com `maxContextChars` já resolvido (4.000 quando não declarado), para o host que
|
|
315
|
+
aplica `projectAssistantContext` por outro canal usar o teto declarado. O `opus check` também reprova
|
|
316
|
+
`maxContextChars` literal fora de 1.000 a 16.000; um valor não literal fica para o registro.
|
|
317
|
+
- **`omitted`.** Cada entrada é `{ path, reason, count? }`, com `reason` igual a `items` (itens
|
|
318
|
+
removidos do fim da lista, `count` diz quantos), `shortened`, `removed` ou `missing`. O caminho é
|
|
319
|
+
o da saída, sem índice de lista: em `removed` e `missing`, é o declarado (ou o do título); em
|
|
320
|
+
`items` e `shortened`, pode ficar abaixo do declarado quando o caminho declarado termina num
|
|
321
|
+
objeto ou numa lista. Um título ausente, nulo ou que não é texto nem número vira `title: null` e
|
|
322
|
+
entra em `omitted` como `missing`, com o aviso no log. O título passa por `toJSON` como na
|
|
323
|
+
serialização, para sair igual com e sem a projeção (uma data vira o texto ISO nos dois casos). O `opus gen` recusa `title` que não
|
|
324
|
+
resolve para texto ou número, fora de lista, em algum ramo não nulo.
|
|
325
|
+
- **Custo do corte.** As etapas 1 e 2 escolhem a maior lista e o texto mais longo por fila de
|
|
326
|
+
prioridade, sem varrer a projeção a cada passo: o custo cresce com o tamanho da saída. Um teste
|
|
327
|
+
roda os cenários num processo filho com prazo, porque o laço é síncrono e o prazo do teste não o
|
|
328
|
+
interromperia.
|
|
329
|
+
- **Segmentos reservados.** `__proto__`, `constructor` e `prototype` não são aceitos como segmento
|
|
330
|
+
de `title` ou `context` no registro, no `opus gen` e, quando literais, no `opus check`; a projeção
|
|
331
|
+
grava as chaves como propriedades próprias.
|
|
332
|
+
- **Caminhos sobrepostos.** Um caminho coberto por outro declarado, como `leads.totalCount` junto de
|
|
333
|
+
`leads`, é ignorado na projeção, porque o mais curto já leva o valor inteiro.
|
|
334
|
+
- **Bloco ao modelo.** A função é `resourceDataBlock`, de `@softize/opus/ai`. O bloco é uma linha de
|
|
335
|
+
JSON entre `<resource-data>` e `</resource-data>` (a etiqueta pode ser trocada por outra em
|
|
336
|
+
minúsculas com hífen). Saem como escapes `\u`, que o JSON lê como o mesmo texto: `<` e `>`; os
|
|
337
|
+
sinais que a normalização NFKC transforma neles; os confusáveis cujo esqueleto contém `<` ou `>`,
|
|
338
|
+
derivados do confusables.txt do Unicode (UTS #39, versão de 2026-08-06); os colchetes angulares
|
|
339
|
+
de outros blocos; os caracteres de controle (C0, DEL, C1 e NEL); os de formatação
|
|
340
|
+
invisíveis; os seletores de variação; os preenchimentos do hangul; os separadores de linha e
|
|
341
|
+
parágrafo; e os espaços que não são o espaço comum. `truncated` e `omitted` entram quando houve
|
|
342
|
+
corte ou quando faltou algum caminho.
|
|
343
|
+
- **`opus gen`.** Um campo sem forma declarada na saída (`z.unknown()`, `z.any()`) aceita qualquer
|
|
344
|
+
caminho abaixo dele, porque não há o que conferir. Um schema que não pôde ser convertido em JSON
|
|
345
|
+
Schema recusa o manifest. Na entrada, além de `id`, campos opcionais sem valor padrão também são
|
|
346
|
+
aceitos: a decisão só recusa outro campo obrigatório.
|
|
347
|
+
- **`label`.** É texto simples, não `I18nRef`, como o `triggerLabel` da Presentation. O `opus copy`
|
|
348
|
+
o inventaria com o papel `title`, porque ele nomeia o assistente na superfície do chat.
|
|
349
|
+
- **Aviso de caminho ausente.** O log usa o código `runtime.assistant_path_missing`, com a action,
|
|
350
|
+
o recurso e os caminhos.
|
|
351
|
+
- **Caminho que termina num contêiner.** Um caminho de `context` que termina num objeto ou numa
|
|
352
|
+
lista projeta tudo o que está abaixo dele, inclusive campos que a saída ganhar depois. O consumidor
|
|
353
|
+
que mantém uma lista de campos proibidos precisa conferir os descendentes, e não só o último
|
|
354
|
+
segmento do caminho. Para isso, `@softize/opus/assistant-schema` exporta
|
|
355
|
+
`assistantContextLeaves(output, context)`: a partir do JSON Schema da saída, como o manifest o
|
|
356
|
+
traz, devolve por caminho os campos folha alcançados e os pontos sem forma declarada ou em que um
|
|
357
|
+
`$ref` volta a si mesmo, por onde qualquer campo pode passar. O subpath publica tipos
|
|
358
|
+
(`assistant-schema.d.mts`) para um teste em TypeScript estrito.
|
|
359
|
+
|
|
360
|
+
## Consequências
|
|
361
|
+
|
|
362
|
+
- A action dona passa a ser a fonte única de leitura, acesso, procedimento e dado ao modelo para um
|
|
363
|
+
tipo de recurso. O consumidor troca tabelas no host, montagem na tela e `if` por tipo por uma
|
|
364
|
+
declaração e duas leituras do runtime.
|
|
365
|
+
- O manifest cresce com `ai` e `assistant`. Projetos que versionam o manifest regeneram com
|
|
366
|
+
`opus gen` na atualização.
|
|
367
|
+
- A Lente passa a mostrar, por action, o que a IA alcança e o que cada assistente entrega ao
|
|
368
|
+
modelo. Depende de uma versão nova da Lente.
|
|
369
|
+
- O campo é opcional e entra numa minor. A regra do `opus check` vale só para quem declara.
|
|
370
|
+
- A conferência de acesso executa a action inteira. Uma action dona pesada fica cara para listar
|
|
371
|
+
conversas; o consumidor mede e, se preciso, escolhe uma `view` mais leve como dona.
|
|
372
|
+
- A existência do procedimento continua fora do alcance do SDK. Uma chave errada só aparece no
|
|
373
|
+
teste do consumidor ou no turno.
|
|
374
|
+
|
|
375
|
+
## Alternativas descartadas
|
|
376
|
+
|
|
377
|
+
### Manter a política no host
|
|
378
|
+
|
|
379
|
+
É o estado atual. Cada consumidor repete tabela de procedimentos, montagem de contexto e rota por
|
|
380
|
+
tipo, e nada disso aparece no manifest nem na Lente.
|
|
381
|
+
|
|
382
|
+
### Declarar dentro de `ai`
|
|
383
|
+
|
|
384
|
+
`ai: { assistant: { ... } }` deixaria tudo que é de IA num lugar. Mas a declaração também define a
|
|
385
|
+
action dona do recurso, usada para conferir acesso em telas e listagens que não são da IA, como o
|
|
386
|
+
histórico de conversas. É o mesmo motivo que levou `effect` e `personalData` para fora de `ai` na
|
|
387
|
+
ADR 0041.
|
|
388
|
+
|
|
389
|
+
### Declarar na Presentation
|
|
390
|
+
|
|
391
|
+
A Presentation é interface, e o recurso precisa do assistente também onde não há Presentation: num
|
|
392
|
+
drawer composto pelo consumidor ou no chat geral, que não tem superfície.
|
|
393
|
+
|
|
394
|
+
### Projeção por função
|
|
395
|
+
|
|
396
|
+
`context: (output) => ...` seria mais flexível, mas não é serializável. Sem uma lista de caminhos,
|
|
397
|
+
o manifest e a Lente não conseguem mostrar quais dados vão ao modelo, que é o objetivo da
|
|
398
|
+
declaração.
|
|
399
|
+
|
|
400
|
+
### Validar o procedimento no `opus check`
|
|
401
|
+
|
|
402
|
+
O check é estático e o registro de procedimentos é dado de banco, diferente em cada ambiente.
|
|
403
|
+
Reprovar ali exigiria acesso ao banco no gate ou uma cópia do registro no repositório.
|
|
404
|
+
|
|
405
|
+
### Aplicar a projeção também às respostas de ferramenta
|
|
406
|
+
|
|
407
|
+
Minimizaria o dado ao modelo, mas a mesma action devolveria coisas diferentes por consumidor, e os
|
|
408
|
+
procedimentos que precisam da saída completa, como editar um relatório pela revisão lida,
|
|
409
|
+
deixariam de funcionar.
|
|
410
|
+
|
|
411
|
+
## Verificação
|
|
412
|
+
|
|
413
|
+
- Testes do SDK:
|
|
414
|
+
- `assistant` numa action que não é `view`, sem `ai` ou sem `personalData` falha no `opus check`;
|
|
415
|
+
- dois contratos com o mesmo `resource` falham no `opus check` e no `register`;
|
|
416
|
+
- `opus gen` recusa `title` ou caminho de `context` ausentes na saída, aceita caminho que existe
|
|
417
|
+
num ramo não nulo de `anyOf` e recusa entrada sem `id` ou com outro campo obrigatório;
|
|
418
|
+
- `readResource` executa a action dona pelo `execute`: a autorização da action vale, o registro
|
|
419
|
+
de auditoria é gerado e a marca de dado pessoal aparece;
|
|
420
|
+
- `readResource` com tipo sem action dona devolve `runtime.assistant_resource_unknown` sem
|
|
421
|
+
executar nada;
|
|
422
|
+
- a projeção devolve só os caminhos declarados, inclusive dentro de listas, e `null` quando um
|
|
423
|
+
campo intermediário é nulo;
|
|
424
|
+
- um caminho ausente em execução é omitido com motivo `missing` e aviso no log, sem falhar;
|
|
425
|
+
- acima do teto, a projeção segue as etapas da seção 3 na ordem, marca `truncated`, lista o que
|
|
426
|
+
saiu em `omitted` e termina mesmo com o teto mínimo;
|
|
427
|
+
- um título acima de 200 caracteres é encurtado e registrado em `omitted`, sem entrar na conta do
|
|
428
|
+
teto;
|
|
429
|
+
- o bloco entregue ao modelo leva `truncated` e `omitted`, e a função de bloco neutraliza o
|
|
430
|
+
delimitador dentro dos valores;
|
|
431
|
+
- o manifest projeta `ai` e `assistant`; `aiTools()` e o `ListTools` do MCP levam a declaração;
|
|
432
|
+
- `opus copy` inventaria `assistant.label`.
|
|
433
|
+
- Documentação e skills do pacote, no mesmo entregável:
|
|
434
|
+
- `docs/protocol.md` ganha o campo na definição de action (seção 2), o princípio da seção 6 desta
|
|
435
|
+
ADR e o verbete no glossário;
|
|
436
|
+
- a página de actions da documentação (`src/ui/docs/content/actions.md`) mostra o campo e a ordem
|
|
437
|
+
canônica com ele;
|
|
438
|
+
- as skills `create-opus-action` e `test-opus-action` (`registry/skills`) ensinam a declarar e a
|
|
439
|
+
testar o assistente.
|
|
440
|
+
- Na Lente: a página de uma action com assistente mostra publicação para a IA, efeito, dado pessoal,
|
|
441
|
+
procedimento e campos enviados ao modelo.
|
|
442
|
+
- Na Grand Brasil, a adoção (ADR 0111 dela) declara as quatro actions donas e troca a rota por tipo
|
|
443
|
+
pela leitura do runtime.
|
package/docs/protocol.md
CHANGED
|
@@ -135,6 +135,16 @@ type ViewAction<In, Out> = ActionBase<In, Out> & {
|
|
|
135
135
|
kind: 'view'
|
|
136
136
|
projection: string[]
|
|
137
137
|
expand?: Record<string, ExpandSpec>
|
|
138
|
+
assistant?: ActionAssistant // torna a view a action dona do recurso (ADR 0053)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type ActionAssistant = {
|
|
142
|
+
resource: string // tipo de recurso, camelCase, único no runtime
|
|
143
|
+
skill: string // chave do procedimento da IA, minúsculas com hífen
|
|
144
|
+
label: string // nome do assistente para pessoas
|
|
145
|
+
title: string // caminho, na saída, do campo que nomeia o recurso
|
|
146
|
+
context: string[] // caminhos, na saída, do dado que pode ir ao modelo
|
|
147
|
+
maxContextChars?: number // teto do JSON de context: padrão 4.000, de 1.000 a 16.000
|
|
138
148
|
}
|
|
139
149
|
```
|
|
140
150
|
|
|
@@ -230,6 +240,47 @@ Output de search action é **sempre** `Paginated<Output>` (ver §6).
|
|
|
230
240
|
**`view`**
|
|
231
241
|
- `projection: string[]` — campos/relações sempre carregados (`['owner', 'company.address']`).
|
|
232
242
|
- `expand?: Record<string, ExpandSpec>` — relações opt-in por request.
|
|
243
|
+
- `assistant?: ActionAssistant` — declara o assistente do recurso que a view lê e a torna a
|
|
244
|
+
**action dona** desse tipo de recurso (ADR 0053). Ver "Assistente do recurso", abaixo.
|
|
245
|
+
|
|
246
|
+
### Assistente do recurso
|
|
247
|
+
|
|
248
|
+
Uma conversa sobre um recurso específico, como um cliente ou um relatório, precisa ler o recurso
|
|
249
|
+
com a permissão de quem conversa, seguir o procedimento próprio daquele tipo e entregar ao modelo só
|
|
250
|
+
o dado necessário. A action `view` que lê o recurso declara as três coisas em `assistant` (ADR 0053).
|
|
251
|
+
|
|
252
|
+
- **Requisitos da action dona.** Ser `view`; ter `ai` habilitado; declarar `personalData`; ter
|
|
253
|
+
entrada em que `id`, do tipo texto, é o único campo obrigatório. Cada `resource` tem uma única
|
|
254
|
+
action dona no runtime. O registro recusa a forma inválida (`runtime.assistant_declaration_invalid`),
|
|
255
|
+
o teto fora do intervalo (`runtime.assistant_context_limit_invalid`) e o dono duplicado
|
|
256
|
+
(`runtime.assistant_duplicate_resource`); o `opus check` (regra `assistant-declaration`) e o
|
|
257
|
+
`opus gen` (caminhos e entrada contra o JSON Schema) cobram o mesmo antes da execução.
|
|
258
|
+
- **Caminhos.** O ponto desce em objetos; um caminho que passa por uma lista vale para cada item;
|
|
259
|
+
um intermediário `null` aparece como `null`. Num campo anulável ou numa união, o `opus gen` aceita
|
|
260
|
+
o caminho que existe em algum ramo não nulo. Um caminho que termina num objeto ou numa lista leva
|
|
261
|
+
tudo abaixo dele (`assistantContextLeaves`, de `@softize/opus/assistant-schema`, lista o que cada
|
|
262
|
+
caminho alcança). `title` aponta para texto ou número, fora de lista.
|
|
263
|
+
- **Leitura.** `runtime.readResource(resource, id, base, { context? })` executa a action dona com
|
|
264
|
+
`{ id }` pelo `execute`: autorização, loaders, auditoria, marca de dado pessoal e span são os da
|
|
265
|
+
action. Devolve `{ ok: true, resource: { kind, id, title, label, skill }, context?, meta }` ou o
|
|
266
|
+
erro da action; um tipo sem dona devolve `runtime.assistant_resource_unknown` sem executar nada.
|
|
267
|
+
`runtime.assistants()` lista as declarações com a action dona.
|
|
268
|
+
- **Projeção.** `projectAssistantContext(assistant, output)`, função pura do core, devolve só os
|
|
269
|
+
caminhos declarados. Acima do teto, corta por etapas até caber: o último item da maior lista; com
|
|
270
|
+
as listas vazias, o texto mais longo pela metade, até 200 caracteres; por fim, campos inteiros, do
|
|
271
|
+
último declarado ao primeiro. Marca `truncated` e registra o que saiu em `omitted`. O título fica
|
|
272
|
+
fora do teto e é encurtado a 200 caracteres. Caminho ausente na saída vai a `omitted` com o motivo
|
|
273
|
+
`missing`, sem falhar a leitura.
|
|
274
|
+
- **Entrega ao modelo.** O host entrega a projeção como bloco de dado delimitado, numa mensagem de
|
|
275
|
+
dado, nunca no prompt de sistema (`resourceDataBlock`, de `@softize/opus/ai`). A projeção governa
|
|
276
|
+
só o que o host entrega por iniciativa própria; quando o modelo chama a action dona como
|
|
277
|
+
ferramenta, recebe a saída completa do contrato.
|
|
278
|
+
|
|
279
|
+
**Princípio: acesso a recurso só por action.** O recurso de um assistente é lido, e o acesso a ele é
|
|
280
|
+
conferido, somente pela action dona. O SDK não oferece outro caminho: a declaração não exporta
|
|
281
|
+
regra de acesso, consulta nem atalho que dispense a action. O Opus não enxerga as rotas HTTP que o
|
|
282
|
+
consumidor registra por conta própria; recomenda-se que o consumidor mantenha um teste que reprove
|
|
283
|
+
rota que leia tabela de recurso ou chame regra de acesso de domínio fora de uma action.
|
|
233
284
|
|
|
234
285
|
### Exemplos por kind
|
|
235
286
|
|
|
@@ -2279,6 +2330,9 @@ chamada, inclusive por IA e MCP.
|
|
|
2279
2330
|
- **`isSystemActor(ctx)`** — responde se a execução foi criada pelo runtime para o sistema (schedule ou job que ele enfileirou). Usado no `authorize` de actions `automatable`. Ver §4.
|
|
2280
2331
|
- **`internal`** — flag que indica action service-to-service apenas; server adapter não expõe como REST público. Ver §2.
|
|
2281
2332
|
- **`effect`** — o que a action faz com o estado: `read` ou `write`. `list` e `view` são leitura pelo `GET` e não declaram; `simple` e `form` declaram. Projetado no manifest, nas ferramentas de IA e no MCP (`readOnlyHint`). Ver ADR 0041.
|
|
2333
|
+
- **`assistant`** — declaração, numa action `view`, do assistente do recurso que ela lê: tipo de recurso, chave do procedimento, nome, campo do título e caminhos do dado ao modelo. Ver §2 e ADR 0053.
|
|
2334
|
+
- **Action dona** — a única action `view` que declara `assistant` para um tipo de recurso; é por ela que o recurso é lido e que o acesso a ele é conferido. Ver §2.
|
|
2335
|
+
- **`runtime.readResource`** — lê um recurso pela action dona, pelo mesmo `execute` de qualquer chamada, e devolve o descritor e, quando pedida, a projeção limitada do dado ao modelo. Ver §2.
|
|
2282
2336
|
- **`personalData`** — a resposta contém dados que identificam pessoas. Com `true`, toda execução bem-sucedida é marcada no `AuditRecord` (`personalData.records`) e no span, qualquer que seja o consumidor. Ver §5 e ADR 0041.
|
|
2283
2337
|
- **Reaction** — declaração que escuta evento(s) e executa handler. Auto-registrada pelo runtime via EventBusAdapter. Pareada com Action (ator inicia × sistema reage). Ver §12.
|
|
2284
2338
|
- **`defineReaction`** — função que constrói uma reaction declarativa.
|
package/docs/releasing.md
CHANGED
|
@@ -59,14 +59,20 @@ O token usado para publicar é exclusivamente o token do npm no `~/.npmrc` globa
|
|
|
59
59
|
pra testar publicação seria a guarda atrapalhando quem está experimentando.
|
|
60
60
|
|
|
61
61
|
**Gate de qualidade**: depois do bump e da materialização, mas antes de publicar, roda
|
|
62
|
-
`pnpm typecheck` + `pnpm test` + `pnpm copy:check` + `base copy check` e
|
|
63
|
-
se qualquer um falhar**. O Opus declara `@softize/base ^2.4.0` em `dependencies`, pois usa
|
|
62
|
+
`pnpm typecheck` + `pnpm test` + `pnpm test:browser` + `pnpm copy:check` + `base copy check` e
|
|
63
|
+
**aborta a release se qualquer um falhar**. O Opus declara `@softize/base ^2.4.0` em `dependencies`, pois usa
|
|
64
64
|
suas APIs públicas de filesystem em runtime, e materializa os artefatos Base no próprio repo;
|
|
65
65
|
a release não baixa uma política ad hoc. Como o
|
|
66
66
|
Opus **ship source** (`.ts`, sem build), essa é a última barreira antes do tarball — sem ela,
|
|
67
67
|
um `tsc` vermelho, inventário desatualizado ou violação da política vaza pro cliente. Emergência (evite):
|
|
68
68
|
`pnpm release --skip-checks`.
|
|
69
69
|
|
|
70
|
+
`pnpm test:browser` roda em Chromium real, pelo modo navegador do Vitest com Playwright, os testes
|
|
71
|
+
que o `happy-dom` não reproduz com fidelidade, como o foco e o Tab da camada flutuante (ADR 0052).
|
|
72
|
+
Ele fica fora do `pnpm test` para que o ciclo comum não dependa de navegador. O Chromium é o da
|
|
73
|
+
revisão fixada pela versão exata do `playwright` nas devDependencies; se a estação não o tiver, o
|
|
74
|
+
comando falha dizendo isso, e `pnpm exec playwright install chromium` o instala.
|
|
75
|
+
|
|
70
76
|
O gate roda `pnpm test`, não `pnpm test:cov`: o threshold de 100% de cobertura em
|
|
71
77
|
`vitest.config.ts` é uma meta aspiracional e **não** faz parte do gate de release. Uma release
|
|
72
78
|
não é bloqueada por cobertura abaixo de 100%; a cobertura é acompanhada com `pnpm test:cov`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Escala de empilhamento
|
|
2
|
+
|
|
3
|
+
> **Status: decidido (out/2026).** As sobreposições do Opus ocupam quatro faixas de `z-index`,
|
|
4
|
+
> definidas pela ADR 0052. Os valores são detalhe de implementação; a ordem entre as faixas é o
|
|
5
|
+
> contrato.
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
Até a camada flutuante, toda sobreposição do Opus usava `z-50`: véu e conteúdo de Dialog e Drawer,
|
|
10
|
+
Popover, Menu e Tooltip. A ordem no DOM desempatava, e o que abria depois ficava por cima. Isso
|
|
11
|
+
bastava enquanto só existiam modais e sobreposições presas a um gatilho.
|
|
12
|
+
|
|
13
|
+
A camada flutuante (`FloatingLayer`) precisa ficar acima de Dialog e Drawer e, ao mesmo tempo,
|
|
14
|
+
abaixo dos diálogos de resposta obrigatória, que a cobrem com o véu (decisão 4 da seção 9 da ADR
|
|
15
|
+
0052). A ordem no DOM não garante isso: o hospedeiro da camada é montado uma vez e continua no
|
|
16
|
+
`body` enquanto modais abrem e fecham depois dele.
|
|
17
|
+
|
|
18
|
+
## Decisão
|
|
19
|
+
|
|
20
|
+
Do fundo para a frente:
|
|
21
|
+
|
|
22
|
+
| Faixa | `z-index` | Quem ocupa |
|
|
23
|
+
| ------------------------- | ------------- | ---------------------------------------------------------------------------------------------- |
|
|
24
|
+
| Conteúdo | `auto` | A página. |
|
|
25
|
+
| Modal | `50` | Dialog, Drawer, seus véus e Popover, Menu e Tooltip abertos fora da camada. |
|
|
26
|
+
| Camada flutuante | `60` | O hospedeiro `div[data-slot="floating-layer"]`. |
|
|
27
|
+
| Resposta obrigatória | `70` | Véu e conteúdo de `Dialog mode="alert"`, que também serve às confirmações do `DialogHost`. |
|
|
28
|
+
| Avisos | `999999999` | O `Toaster`, com o valor do Sonner. |
|
|
29
|
+
|
|
30
|
+
- Dentro de uma faixa, a ordem no DOM continua desempatando, como antes.
|
|
31
|
+
- O hospedeiro da camada cria um contexto de empilhamento próprio. Popover, Menu, Tooltip, Dialog e
|
|
32
|
+
Drawer abertos de dentro dela são portados para o hospedeiro e mantêm o `z-50` lá dentro, acima
|
|
33
|
+
do painel que os abriu.
|
|
34
|
+
- Popover, Menu e Tooltip abertos de dentro de um alerta herdam a faixa do alerta (`z-70`); sem
|
|
35
|
+
isso, ficariam por baixo do véu que o próprio alerta desenha.
|
|
36
|
+
- Componente novo que abre uma sobreposição usa as faixas acima e lê o contêiner da camada; o
|
|
37
|
+
teste `tests/architecture/floating-layer.test.ts` cobra o contêiner de todo portal.
|
|
38
|
+
- Um alerta em animação de saída deixa de receber clique (`pointer-events: none`). Sem isso, o véu
|
|
39
|
+
dele, ainda em `z-70`, cobriria por cerca de 150 ms o Dialog que a resposta abre na faixa de
|
|
40
|
+
baixo.
|
|
41
|
+
|
|
42
|
+
## Consequências
|
|
43
|
+
|
|
44
|
+
- Um consumidor que posiciona elementos próprios com `z-index` acima de `50` passa a competir com a
|
|
45
|
+
camada. Use a camada para o que precisa ficar acima dos modais, em vez de subir o `z-index`.
|
|
46
|
+
- Um elemento do consumidor com `z-index` entre `51` e `69` ficava acima de um alerta, que estava em
|
|
47
|
+
`z-50`, e agora fica abaixo dele.
|
|
48
|
+
- Uma sobreposição de outra biblioteca que fixa `z-50` e é aberta de dentro de um alerta fica abaixo
|
|
49
|
+
do véu do alerta, porque não lê a faixa que os primitives do Opus herdam. Suba a faixa dela para
|
|
50
|
+
`z-70` ou troque-a por um primitive do Opus.
|
|
51
|
+
- Mudar um valor exige mudar todos os pontos que dependem dele: `overlayClasses` e o conteúdo em
|
|
52
|
+
`dialog.tsx` e `drawer.tsx`, o hospedeiro em `floating-layer.tsx` e a faixa do alerta em
|
|
53
|
+
`modal-layers.ts`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softize/opus",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.2.0",
|
|
4
4
|
"description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"./introspect": {
|
|
21
21
|
"default": "./bin/lib/introspect.mjs"
|
|
22
22
|
},
|
|
23
|
+
"./assistant-schema": {
|
|
24
|
+
"types": "./bin/lib/assistant-schema.d.mts",
|
|
25
|
+
"default": "./bin/lib/assistant-schema.mjs"
|
|
26
|
+
},
|
|
23
27
|
"./init": {
|
|
24
28
|
"default": "./bin/lib/init.mjs"
|
|
25
29
|
},
|
|
@@ -362,6 +366,7 @@
|
|
|
362
366
|
"@opentelemetry/api": "^1.9.1",
|
|
363
367
|
"@opentelemetry/context-async-hooks": "^2.10.0",
|
|
364
368
|
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
369
|
+
"@tailwindcss/vite": "^4.3.2",
|
|
365
370
|
"@tanstack/react-query": "^5.62.0",
|
|
366
371
|
"@testing-library/dom": "^10.4.0",
|
|
367
372
|
"@testing-library/react": "^16.1.0",
|
|
@@ -371,6 +376,7 @@
|
|
|
371
376
|
"@types/pg": "^8.11.0",
|
|
372
377
|
"@types/react": "^19.0.0",
|
|
373
378
|
"@types/react-dom": "^19.0.0",
|
|
379
|
+
"@vitest/browser": "2.1.9",
|
|
374
380
|
"@vitest/coverage-v8": "^2.1.0",
|
|
375
381
|
"better-sqlite3": "^11.0.0",
|
|
376
382
|
"bullmq": "^5.0.0",
|
|
@@ -381,9 +387,11 @@
|
|
|
381
387
|
"node-cron": "^3.0.3",
|
|
382
388
|
"pg": "^8.13.0",
|
|
383
389
|
"pino": "^9.5.0",
|
|
390
|
+
"playwright": "1.62.1",
|
|
384
391
|
"react": "^19.0.0",
|
|
385
392
|
"react-dom": "^19.0.0",
|
|
386
393
|
"react-hook-form": "^7.54.0",
|
|
394
|
+
"tailwindcss": "^4.3.2",
|
|
387
395
|
"tsx": "^4.19.0",
|
|
388
396
|
"vite": "^6.0.0",
|
|
389
397
|
"vitest": "^2.1.0",
|
|
@@ -400,6 +408,7 @@
|
|
|
400
408
|
"copy:check": "node ./bin/cli.mjs copy --check",
|
|
401
409
|
"typecheck": "tsc --noEmit",
|
|
402
410
|
"test": "vitest run",
|
|
411
|
+
"test:browser": "vitest run --config vitest.browser.config.ts",
|
|
403
412
|
"test:watch": "vitest",
|
|
404
413
|
"test:cov": "vitest run --coverage",
|
|
405
414
|
"registry:up": "npx -y verdaccio --config ~/.config/verdaccio/config.yaml",
|