@softize/opus 25.1.0 → 25.3.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 +66 -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 +4 -0
- package/bin/lib/gen-runner.mjs +31 -2
- package/bin/lib/gen-stubs.mjs +3 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0053-read-action-declares-the-resource-assistant.md +443 -0
- package/docs/adr/0054-action-list-grouping-is-a-declared-server-cut.md +82 -0
- package/docs/protocol.md +59 -1
- package/package.json +5 -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/actions.ts +5 -2
- package/src/core/assistant.ts +545 -0
- package/src/core/contracts.ts +2 -0
- package/src/core/index.ts +23 -0
- package/src/core/list-grouping.ts +45 -0
- package/src/core/runtime.ts +138 -0
- package/src/core/types.ts +39 -0
- package/src/mcp/index.ts +3 -0
- package/src/ui/components/patterns/grouped-list.ts +88 -0
- package/src/ui/components/patterns/list.tsx +395 -61
- package/src/ui/docs/content/action-list.md +42 -9
- 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/mcp.md +3 -1
- package/src/ui/drivers/context.tsx +50 -0
- package/src/ui/drivers/react.tsx +9 -69
- package/src/ui/meta.ts +1 -1
package/bin/lib/gen-manifest.mjs
CHANGED
|
@@ -55,6 +55,9 @@ function shapeAction(a) {
|
|
|
55
55
|
automatable: a.automatable,
|
|
56
56
|
effect: a.effect ?? null,
|
|
57
57
|
personalData: a.personalData ?? null,
|
|
58
|
+
// ADR 0053: a publicação para a IA e, na action dona, o assistente do recurso.
|
|
59
|
+
ai: a.ai ?? { enabled: false, description: null, destructive: false, requiresConfirmation: false },
|
|
60
|
+
...(a.assistant !== undefined && a.assistant !== null ? { assistant: a.assistant } : {}),
|
|
58
61
|
internal: a.internal,
|
|
59
62
|
successStatus: a.successStatus,
|
|
60
63
|
input: a.input,
|
|
@@ -77,6 +80,7 @@ function shapeAction(a) {
|
|
|
77
80
|
out.paginate = a.paginate;
|
|
78
81
|
out.text = a.text;
|
|
79
82
|
out.periods = a.periods;
|
|
83
|
+
if (a.grouping !== undefined) out.grouping = a.grouping;
|
|
80
84
|
}
|
|
81
85
|
if (a.kind === "view") {
|
|
82
86
|
out.projection = a.projection ?? [];
|
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* input: JSONSchema, // zod-to-json-schema
|
|
44
44
|
* output: JSONSchema, // zod-to-json-schema
|
|
45
45
|
* fields?: Record<string, FieldSpecSerialized>,
|
|
46
|
-
* filters?, sort?, paginate?, text?, periods?, //
|
|
46
|
+
* filters?, sort?, paginate?, text?, periods?, grouping?, // list
|
|
47
47
|
* projection?, expand?, // view
|
|
48
48
|
* background?: boolean,
|
|
49
49
|
* emits?: string[],
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
* automatable?: boolean, // aceita execução pelo sistema (ADR 0028)
|
|
54
54
|
* effect: 'read'|'write'|null, // efeito resolvido; list/view = read (ADR 0041)
|
|
55
55
|
* personalData: boolean|null, // devolve dado pessoal; null = não declarado
|
|
56
|
+
* ai: { enabled, description, destructive, requiresConfirmation }, // ADR 0053
|
|
57
|
+
* assistant?: { resource, skill, label, title, context, maxContextChars }, // view dona
|
|
56
58
|
* internal?: boolean,
|
|
57
59
|
* rateLimit?: { window, max },
|
|
58
60
|
* confirm?: ConfirmSpec,
|
|
@@ -67,6 +69,7 @@
|
|
|
67
69
|
|
|
68
70
|
import path from "node:path";
|
|
69
71
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
72
|
+
import { assistantSchemaIssues, serializeAiConfig } from "./assistant-schema.mjs";
|
|
70
73
|
|
|
71
74
|
const __filename = fileURLToPath(import.meta.url);
|
|
72
75
|
const __dirname = path.dirname(__filename);
|
|
@@ -90,7 +93,7 @@ async function main() {
|
|
|
90
93
|
const opusCoreUrl = pathToFileURL(
|
|
91
94
|
path.join(PACKAGE_ROOT, "src", "core", "index.ts"),
|
|
92
95
|
).href;
|
|
93
|
-
const { flattenDomain, domainPurges, declaredJsonSchema } = await import(opusCoreUrl);
|
|
96
|
+
const { flattenDomain, domainPurges, declaredJsonSchema, publicAssistant } = await import(opusCoreUrl);
|
|
94
97
|
|
|
95
98
|
const opusSeedUrl = pathToFileURL(
|
|
96
99
|
path.join(PACKAGE_ROOT, "src", "seed", "index.ts"),
|
|
@@ -138,6 +141,7 @@ async function main() {
|
|
|
138
141
|
const ctx = {
|
|
139
142
|
flattenDomain,
|
|
140
143
|
domainPurges,
|
|
144
|
+
publicAssistant,
|
|
141
145
|
// Standard Schema que declara o próprio JSON Schema (ADR 0051) dispensa o zod.
|
|
142
146
|
toJsonSchema: (schema) =>
|
|
143
147
|
declaredJsonSchema(schema, { target: "openapi-3.0" }) ??
|
|
@@ -393,6 +397,8 @@ function serializeAction(action, ctx) {
|
|
|
393
397
|
// ADR 0041: `list` e `view` são leitura pelo protocolo; null = não declarado.
|
|
394
398
|
effect: action.kind === "list" || action.kind === "view" ? "read" : (action.effect ?? null),
|
|
395
399
|
personalData: typeof action.personalData === "boolean" ? action.personalData : null,
|
|
400
|
+
// ADR 0053: a publicação para a IA entra no manifest, e a action dona, o assistente.
|
|
401
|
+
ai: serializeAiConfig(action.ai),
|
|
396
402
|
internal: action.internal === true,
|
|
397
403
|
successStatus: action.successStatus,
|
|
398
404
|
input: safeToJsonSchema(action.input, ctx),
|
|
@@ -427,6 +433,7 @@ function serializeAction(action, ctx) {
|
|
|
427
433
|
base.paginate = action.paginate ?? null;
|
|
428
434
|
base.text = action.text ?? null;
|
|
429
435
|
base.periods = Array.isArray(action.periods) ? action.periods : [];
|
|
436
|
+
if (action.grouping !== undefined) base.grouping = action.grouping;
|
|
430
437
|
}
|
|
431
438
|
|
|
432
439
|
if (action.kind === "view") {
|
|
@@ -434,6 +441,20 @@ function serializeAction(action, ctx) {
|
|
|
434
441
|
base.expand = action.expand ?? null;
|
|
435
442
|
}
|
|
436
443
|
|
|
444
|
+
if (action.kind === "view" && action.assistant !== undefined && action.assistant !== null) {
|
|
445
|
+
const issues = assistantSchemaIssues({
|
|
446
|
+
assistant: action.assistant,
|
|
447
|
+
input: base.input,
|
|
448
|
+
output: base.output,
|
|
449
|
+
});
|
|
450
|
+
if (issues.length > 0) {
|
|
451
|
+
throw declarationError(
|
|
452
|
+
`O assistente da action "${action.name}" não confere com o contrato (ADR 0053): ${issues.join("; ")}.`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
base.assistant = ctx.publicAssistant(action.assistant);
|
|
456
|
+
}
|
|
457
|
+
|
|
437
458
|
return base;
|
|
438
459
|
}
|
|
439
460
|
|
|
@@ -605,6 +626,14 @@ function emitError(message) {
|
|
|
605
626
|
process.exit(1);
|
|
606
627
|
}
|
|
607
628
|
|
|
629
|
+
/** Erro de declaração do consumidor: a mensagem basta, sem a pilha do runner. */
|
|
630
|
+
function declarationError(message) {
|
|
631
|
+
const err = new Error(message);
|
|
632
|
+
err.name = "OpusDeclarationError";
|
|
633
|
+
return err;
|
|
634
|
+
}
|
|
635
|
+
|
|
608
636
|
main().catch((err) => {
|
|
637
|
+
if (err?.name === "OpusDeclarationError") return emitError(err.message);
|
|
609
638
|
emitError(`runner crash: ${err.stack ?? err.message ?? String(err)}`);
|
|
610
639
|
});
|
package/bin/lib/gen-stubs.mjs
CHANGED
|
@@ -149,6 +149,9 @@ function renderActionStub(action) {
|
|
|
149
149
|
const filters = stripNullsFromFields(action.filters)
|
|
150
150
|
lines.push(` filters: ${formatObjectLiteral(filters, ' ')},`)
|
|
151
151
|
}
|
|
152
|
+
if (action.grouping && Array.isArray(action.grouping.fields)) {
|
|
153
|
+
lines.push(` grouping: ${JSON.stringify(action.grouping)},`)
|
|
154
|
+
}
|
|
152
155
|
}
|
|
153
156
|
if (Array.isArray(action.invalidates) && action.invalidates.length > 0) {
|
|
154
157
|
lines.push(` invalidates: ${JSON.stringify(action.invalidates)},`)
|
package/bin/lib/mcp.mjs
CHANGED
|
@@ -42,7 +42,7 @@ export function buildServer() {
|
|
|
42
42
|
server.registerTool(
|
|
43
43
|
'opus_check',
|
|
44
44
|
{
|
|
45
|
-
description: 'Valida as convenções das actions (régua) — defineAction e o split defineContract+bindAction: naming, kind, ordem dos campos, export, requires-sem-authorize, ignored-by-kind, soft-delete-retention. Retorna findings.',
|
|
45
|
+
description: 'Valida as convenções das actions (régua) — defineAction e o split defineContract+bindAction: naming, kind, ordem dos campos, export, requires-sem-authorize, ignored-by-kind, assistant-declaration, soft-delete-retention. Retorna findings.',
|
|
46
46
|
inputSchema: { dir: z.string().optional() },
|
|
47
47
|
},
|
|
48
48
|
async ({ dir }) => asText(await scanDir(dirOf(dir))),
|
|
@@ -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.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# ADR 0054 — Agrupamento da ActionList é um recorte declarado e consultado no servidor
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-03.
|
|
5
|
+
- **Relacionadas:** ADR 0013 (Presentation orientada a action) e ADR 0024 (preenchimento das
|
|
6
|
+
regiões de lista).
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
Listagens operacionais precisam, em alguns casos, comparar conjuntos dentro do mesmo recorte. A
|
|
11
|
+
fila de eventos da Grand Brasil é o primeiro caso: a pessoa quer reunir os eventos por status sem
|
|
12
|
+
perder busca, filtros, ordenação, contagens ou paginação.
|
|
13
|
+
|
|
14
|
+
Agrupar apenas os itens carregados criaria uma representação falsa. Uma página de cinquenta itens
|
|
15
|
+
não permite concluir quantos registros existem em cada grupo, e trocar de página faria grupos
|
|
16
|
+
sumirem ou mudarem de tamanho. Acrescentar um envelope agregado ao protocolo resolveria isso com
|
|
17
|
+
uma consulta, mas obrigaria todo handler e adapter a aprender uma segunda forma de resposta antes de
|
|
18
|
+
existir evidência de que o custo compensa.
|
|
19
|
+
|
|
20
|
+
O campo também não pode ser inferido de toda coluna ou filtro. Texto livre e lookup podem ter
|
|
21
|
+
cardinalidade alta, data e número exigem faixas, e nem toda dimensão fechada faz sentido como
|
|
22
|
+
organização da lista. A decisão precisa continuar no contrato de produto.
|
|
23
|
+
|
|
24
|
+
## Decisão
|
|
25
|
+
|
|
26
|
+
### 1. A action declara as dimensões permitidas
|
|
27
|
+
|
|
28
|
+
Uma `ListAction` pode declarar:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
grouping: {
|
|
32
|
+
fields: ['status', 'stage'],
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Cada nome referencia um filtro `select` com opções `static` ou `dictionary`. O controle do filtro
|
|
37
|
+
pode aceitar uma ou várias opções; cada consulta de grupo envia um único valor na forma esperada
|
|
38
|
+
pelo filtro. A declaração não define agrupamento inicial: a lista continua plana até a pessoa
|
|
39
|
+
escolher “Agrupar por” em **Exibição**. O estado usa `groupBy` e a URL usa `group=<campo>`.
|
|
40
|
+
|
|
41
|
+
O Opus rejeita dimensões ausentes, filtros de outro tipo, lookups, conjuntos estáticos sem opções
|
|
42
|
+
fechadas e actions sem `paginate: 'offset'`. A interface oferece apenas dicionários resolvidos com
|
|
43
|
+
até doze opções por dimensão; acima disso, a declaração não é adequada para agrupamento operacional.
|
|
44
|
+
|
|
45
|
+
### 2. Cada grupo é uma consulta da mesma ListAction
|
|
46
|
+
|
|
47
|
+
O agrupamento preserva o input base, a busca, o período, os demais filtros e a ordenação. Para cada
|
|
48
|
+
opção da dimensão, o Opus executa a mesma action com o valor do grupo aplicado ao filtro. Um filtro
|
|
49
|
+
já aplicado à dimensão restringe quais grupos aparecem; não é removido nem reinterpretado.
|
|
50
|
+
|
|
51
|
+
Cada resposta precisa devolver `total`. A ausência desse dado vira o estado de erro da lista, sem
|
|
52
|
+
lançar uma exceção durante o render. Esse total nomeia o grupo e governa sua paginação. O primeiro
|
|
53
|
+
recorte usa o limite da lista; **Carregar mais** busca a página seguinte daquele grupo e acrescenta
|
|
54
|
+
os itens, sem reiniciar os demais. A invalidação ou a atualização manual refaz todas as consultas.
|
|
55
|
+
|
|
56
|
+
Essa primeira implementação aceita o custo de até doze consultas em paralelo. Um envelope de
|
|
57
|
+
facetas ou uma action agregada só será introduzido quando medição real justificar outra fronteira.
|
|
58
|
+
|
|
59
|
+
### 3. O agrupamento pertence à tabela
|
|
60
|
+
|
|
61
|
+
A tabela mantém um único cabeçalho e abre um `tbody` por grupo. O cabeçalho do grupo é recolhível,
|
|
62
|
+
expõe `aria-expanded`, apresenta o valor pelo dicionário quando houver e mostra a contagem correta,
|
|
63
|
+
inclusive zero. O agrupamento não se aplica a `children` nem a views alternativas; ao sair da tabela,
|
|
64
|
+
os dados voltam à consulta plana.
|
|
65
|
+
|
|
66
|
+
Busca, filtros e período são aplicados antes do agrupamento; ordenação vale dentro de cada grupo. A
|
|
67
|
+
seleção em lote cobre os itens carregados dos grupos abertos. Recolher um grupo remove da seleção os
|
|
68
|
+
itens dele, para nenhuma ação operar sobre linhas invisíveis.
|
|
69
|
+
|
|
70
|
+
Se uma consulta falhar, a lista apresenta o erro padrão e permite refazer o conjunto. Se todos os
|
|
71
|
+
totais forem zero, há um único estado vazio. Não existe paginador global enquanto o agrupamento está
|
|
72
|
+
ativo; o rodapé mostra apenas a soma dos totais.
|
|
73
|
+
|
|
74
|
+
## Consequências
|
|
75
|
+
|
|
76
|
+
- O contrato distingue capacidade de agrupamento de uma seleção corrente.
|
|
77
|
+
- Contagens e páginas continuam autoritativas no servidor; não há agrupamento parcial silencioso.
|
|
78
|
+
- O número de requisições cresce com as opções da dimensão, de forma explícita e limitada.
|
|
79
|
+
- O primeiro consumidor deve testar que a soma dos totais agrupados coincide com o total da lista
|
|
80
|
+
plana para o mesmo recorte.
|
|
81
|
+
- Uma futura otimização de protocolo pode preservar `grouping` e `groupBy`, trocando apenas como os
|
|
82
|
+
dados chegam ao cliente.
|