@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/assistant-schema.d.mts +55 -0
  4. package/bin/lib/assistant-schema.mjs +286 -0
  5. package/bin/lib/check.mjs +173 -0
  6. package/bin/lib/copy.mjs +2 -0
  7. package/bin/lib/gen-manifest.mjs +4 -0
  8. package/bin/lib/gen-runner.mjs +31 -2
  9. package/bin/lib/gen-stubs.mjs +3 -0
  10. package/bin/lib/mcp.mjs +1 -1
  11. package/docs/adr/0053-read-action-declares-the-resource-assistant.md +443 -0
  12. package/docs/adr/0054-action-list-grouping-is-a-declared-server-cut.md +82 -0
  13. package/docs/protocol.md +59 -1
  14. package/package.json +5 -1
  15. package/registry/skills/create-opus-action/SKILL.md +8 -3
  16. package/registry/skills/create-opus-action/references/contract-and-binding.md +24 -1
  17. package/registry/skills/create-opus-action/references/evaluations.md +3 -0
  18. package/registry/skills/test-opus-action/SKILL.md +4 -1
  19. package/registry/skills/test-opus-action/references/evaluations.md +2 -0
  20. package/registry/skills/test-opus-action/references/harness.md +14 -0
  21. package/src/ai/index.ts +3 -0
  22. package/src/ai/resource-context.ts +113 -0
  23. package/src/core/actions.ts +5 -2
  24. package/src/core/assistant.ts +545 -0
  25. package/src/core/contracts.ts +2 -0
  26. package/src/core/index.ts +23 -0
  27. package/src/core/list-grouping.ts +45 -0
  28. package/src/core/runtime.ts +138 -0
  29. package/src/core/types.ts +39 -0
  30. package/src/mcp/index.ts +3 -0
  31. package/src/ui/components/patterns/grouped-list.ts +88 -0
  32. package/src/ui/components/patterns/list.tsx +395 -61
  33. package/src/ui/docs/content/action-list.md +42 -9
  34. package/src/ui/docs/content/actions.md +97 -1
  35. package/src/ui/docs/content/ai.md +4 -2
  36. package/src/ui/docs/content/cli.md +2 -1
  37. package/src/ui/docs/content/mcp.md +3 -1
  38. package/src/ui/drivers/context.tsx +50 -0
  39. package/src/ui/drivers/react.tsx +9 -69
  40. package/src/ui/meta.ts +1 -1
package/docs/protocol.md CHANGED
@@ -129,12 +129,23 @@ type ListAction<In, Out> = ActionBase<In, Paginated<Out>> & {
129
129
  paginate?: 'cursor' | 'offset' | false
130
130
  text?: { fields: string[] }
131
131
  periods?: { value: string, label: I18nRef }[]
132
+ grouping?: { fields: string[] }
132
133
  }
133
134
 
134
135
  type ViewAction<In, Out> = ActionBase<In, Out> & {
135
136
  kind: 'view'
136
137
  projection: string[]
137
138
  expand?: Record<string, ExpandSpec>
139
+ assistant?: ActionAssistant // torna a view a action dona do recurso (ADR 0053)
140
+ }
141
+
142
+ type ActionAssistant = {
143
+ resource: string // tipo de recurso, camelCase, único no runtime
144
+ skill: string // chave do procedimento da IA, minúsculas com hífen
145
+ label: string // nome do assistente para pessoas
146
+ title: string // caminho, na saída, do campo que nomeia o recurso
147
+ context: string[] // caminhos, na saída, do dado que pode ir ao modelo
148
+ maxContextChars?: number // teto do JSON de context: padrão 4.000, de 1.000 a 16.000
138
149
  }
139
150
  ```
140
151
 
@@ -195,7 +206,10 @@ type OptionsSpec =
195
206
 
196
207
  **`search`**
197
208
  - `filters?: Record<string, FilterSpec>` — filtros disponíveis (chave é nome).
198
- - `sort?`, `paginate?`, `text?`, `periods?` — ver tipos abaixo.
209
+ - `sort?`, `paginate?`, `text?`, `periods?`, `grouping?` — ver tipos abaixo.
210
+ - `grouping.fields` referencia filtros `select` com opções fechadas (`static` ou
211
+ `dictionary`). A interface consulta cada valor como um recorte da mesma action; não agrupa só a
212
+ página carregada.
199
213
 
200
214
  ```ts
201
215
  type FilterSpec = {
@@ -230,6 +244,47 @@ Output de search action é **sempre** `Paginated<Output>` (ver §6).
230
244
  **`view`**
231
245
  - `projection: string[]` — campos/relações sempre carregados (`['owner', 'company.address']`).
232
246
  - `expand?: Record<string, ExpandSpec>` — relações opt-in por request.
247
+ - `assistant?: ActionAssistant` — declara o assistente do recurso que a view lê e a torna a
248
+ **action dona** desse tipo de recurso (ADR 0053). Ver "Assistente do recurso", abaixo.
249
+
250
+ ### Assistente do recurso
251
+
252
+ Uma conversa sobre um recurso específico, como um cliente ou um relatório, precisa ler o recurso
253
+ com a permissão de quem conversa, seguir o procedimento próprio daquele tipo e entregar ao modelo só
254
+ o dado necessário. A action `view` que lê o recurso declara as três coisas em `assistant` (ADR 0053).
255
+
256
+ - **Requisitos da action dona.** Ser `view`; ter `ai` habilitado; declarar `personalData`; ter
257
+ entrada em que `id`, do tipo texto, é o único campo obrigatório. Cada `resource` tem uma única
258
+ action dona no runtime. O registro recusa a forma inválida (`runtime.assistant_declaration_invalid`),
259
+ o teto fora do intervalo (`runtime.assistant_context_limit_invalid`) e o dono duplicado
260
+ (`runtime.assistant_duplicate_resource`); o `opus check` (regra `assistant-declaration`) e o
261
+ `opus gen` (caminhos e entrada contra o JSON Schema) cobram o mesmo antes da execução.
262
+ - **Caminhos.** O ponto desce em objetos; um caminho que passa por uma lista vale para cada item;
263
+ um intermediário `null` aparece como `null`. Num campo anulável ou numa união, o `opus gen` aceita
264
+ o caminho que existe em algum ramo não nulo. Um caminho que termina num objeto ou numa lista leva
265
+ tudo abaixo dele (`assistantContextLeaves`, de `@softize/opus/assistant-schema`, lista o que cada
266
+ caminho alcança). `title` aponta para texto ou número, fora de lista.
267
+ - **Leitura.** `runtime.readResource(resource, id, base, { context? })` executa a action dona com
268
+ `{ id }` pelo `execute`: autorização, loaders, auditoria, marca de dado pessoal e span são os da
269
+ action. Devolve `{ ok: true, resource: { kind, id, title, label, skill }, context?, meta }` ou o
270
+ erro da action; um tipo sem dona devolve `runtime.assistant_resource_unknown` sem executar nada.
271
+ `runtime.assistants()` lista as declarações com a action dona.
272
+ - **Projeção.** `projectAssistantContext(assistant, output)`, função pura do core, devolve só os
273
+ caminhos declarados. Acima do teto, corta por etapas até caber: o último item da maior lista; com
274
+ as listas vazias, o texto mais longo pela metade, até 200 caracteres; por fim, campos inteiros, do
275
+ último declarado ao primeiro. Marca `truncated` e registra o que saiu em `omitted`. O título fica
276
+ fora do teto e é encurtado a 200 caracteres. Caminho ausente na saída vai a `omitted` com o motivo
277
+ `missing`, sem falhar a leitura.
278
+ - **Entrega ao modelo.** O host entrega a projeção como bloco de dado delimitado, numa mensagem de
279
+ dado, nunca no prompt de sistema (`resourceDataBlock`, de `@softize/opus/ai`). A projeção governa
280
+ só o que o host entrega por iniciativa própria; quando o modelo chama a action dona como
281
+ ferramenta, recebe a saída completa do contrato.
282
+
283
+ **Princípio: acesso a recurso só por action.** O recurso de um assistente é lido, e o acesso a ele é
284
+ conferido, somente pela action dona. O SDK não oferece outro caminho: a declaração não exporta
285
+ regra de acesso, consulta nem atalho que dispense a action. O Opus não enxerga as rotas HTTP que o
286
+ consumidor registra por conta própria; recomenda-se que o consumidor mantenha um teste que reprove
287
+ rota que leia tabela de recurso ou chame regra de acesso de domínio fora de uma action.
233
288
 
234
289
  ### Exemplos por kind
235
290
 
@@ -2279,6 +2334,9 @@ chamada, inclusive por IA e MCP.
2279
2334
  - **`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
2335
  - **`internal`** — flag que indica action service-to-service apenas; server adapter não expõe como REST público. Ver §2.
2281
2336
  - **`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.
2337
+ - **`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.
2338
+ - **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.
2339
+ - **`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
2340
  - **`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
2341
  - **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
2342
  - **`defineReaction`** — função que constrói uma reaction declarativa.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "25.1.0",
3
+ "version": "25.3.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
  },
@@ -16,6 +16,7 @@ registrados no runtime do projeto.
16
16
  - nome `<resource>.<verb>` e `kind` (`simple`, `form`, `list` ou `view`);
17
17
  - comportamento, autorização, input, output, erros e efeitos esperados;
18
18
  - se a action só lê ou altera estado e se a resposta contém dados que identificam pessoas;
19
+ - se a action `view` é a leitura de um tipo de recurso com conversa própria da IA (assistente);
19
20
  - caminhos reais de `shared` e `api` adotados pelo repo.
20
21
 
21
22
  ## Procedimento
@@ -28,13 +29,17 @@ registrados no runtime do projeto.
28
29
  `effect: 'write'` e deixa `personalData` comentado de propósito: a ausência aparece no aviso do
29
30
  `opus check` até alguém decidir, porque um `false` preenchido por padrão esconderia exposição de
30
31
  dado pessoal.
31
- 6. Exportar e registrar o binding no domínio/runtime existente.
32
- 7. Carregar e seguir `$test-opus-action` para cobrir contrato, sucesso, erros e efeitos relevantes.
32
+ 6. Quando a `view` for a action dona de um tipo de recurso com assistente, declarar `assistant`
33
+ depois de `ai`, com `ai` habilitado, `personalData` declarado e entrada `{ id }`, conforme
34
+ [contrato e binding](references/contract-and-binding.md).
35
+ 7. Exportar e registrar o binding no domínio/runtime existente.
36
+ 8. Carregar e seguir `$test-opus-action` para cobrir contrato, sucesso, erros e efeitos relevantes.
33
37
 
34
38
  ## Verificação
35
39
 
36
40
  Rodar `opus check <escopo>`, typecheck e testes do domínio. Se o projeto gera manifest,
37
- OpenAPI ou docs, regenerar e conferir o diff. Regenerar também o inventário com `opus copy`
41
+ OpenAPI ou docs, regenerar e conferir o diff; com `assistant`, o `opus gen` confere `title`,
42
+ `context` e a entrada contra os schemas. Regenerar também o inventário com `opus copy`
38
43
  e executar `base copy check` quando o contrato declarar texto humano.
39
44
 
40
45
  ## Limites
@@ -14,6 +14,29 @@ O contrato também declara o que a action faz com os dados (ADR 0041): `effect`
14
14
  em todos os kinds. Os dois fatos alimentam a auditoria de exposição de dado pessoal, as ferramentas
15
15
  de IA e as anotações do MCP.
16
16
 
17
+ ## Assistente do recurso
18
+
19
+ Uma `view` que lê um tipo de recurso com conversa própria da IA declara `assistant` e passa a ser a
20
+ action dona do recurso (ADR 0053): é por ela que o recurso é lido e o acesso, conferido.
21
+
22
+ ```ts
23
+ ai: true,
24
+ assistant: {
25
+ resource: 'customer', // camelCase, uma única action dona por recurso
26
+ skill: 'review-customer', // chave do procedimento, mantido pelo produto
27
+ label: 'Assistente do cliente',
28
+ title: 'name', // caminho da saída que nomeia o recurso
29
+ context: ['stage', 'leads.totalCount'], // só o que orienta a conversa
30
+ },
31
+ ```
32
+
33
+ - Exigir `ai` habilitado, `personalData` declarado e entrada em que `id`, do tipo texto, é o único
34
+ campo obrigatório.
35
+ - Escolher `context` campo a campo e deixar fora contato, documento, endereço e texto livre sobre
36
+ pessoas; o título já vai ao modelo.
37
+ - Não criar rota, consulta ou regra de acesso paralela para ler o recurso: o produto usa
38
+ `runtime.readResource` e confere as chaves de `skill` com `runtime.assistants()` num teste.
39
+
17
40
  Ordem cobrada pelo check: identidade → documentação → input/output e natureza (`effect`,
18
- `personalData`) → autorização → handler → comportamento. Campos específicos do kind permanecem junto do grupo ao qual
41
+ `personalData`) → autorização → handler → comportamento → integração (`ai`, `assistant`). Campos específicos do kind permanecem junto do grupo ao qual
19
42
  servem e o scaffold acompanha a forma atual do SDK.
@@ -6,3 +6,6 @@
6
6
  - Execução: “Crie `customer.list`, que lista clientes com nome e telefone” resulta em
7
7
  `personalData: true` declarado; “Crie `sales.performance`, que soma vendas por mês” resulta em
8
8
  `effect: 'read'` e `personalData: false`.
9
+ - Execução: “Crie `customer.view`, que o assistente do cliente usa para ler o cadastro” resulta em
10
+ `assistant` depois de `ai`, `personalData` declarado, entrada `{ id }` e `context` sem contato
11
+ nem documento; `opus check` e `opus gen` passam.
@@ -18,7 +18,10 @@ infraestrutura real quando a unidade é o contrato.
18
18
  pelas opções do harness.
19
19
  3. Cobrir sucesso, input inválido, autorização negada e erros de negócio materiais.
20
20
  4. Afirmar output e efeitos observáveis como `emitted`, `logged` e estado do adapter fake.
21
- 5. Adicionar integração somente para a fronteira não provada pela unidade: banco, transporte,
21
+ 5. Quando a action declarar `assistant`, ler o recurso por `runtime.readResource` num runtime com a
22
+ action registrada e provar a autorização da action, o descritor e o `context.data` só com os
23
+ caminhos declarados, conforme o [harness](references/harness.md).
24
+ 6. Adicionar integração somente para a fronteira não provada pela unidade: banco, transporte,
22
25
  registro real ou wiring entre actions.
23
26
 
24
27
  ## Verificação
@@ -3,3 +3,5 @@
3
3
  - Dispara: “Cubra com testes a action Opus de arquivar cliente.”
4
4
  - Não dispara: “Teste este parser CSV que não usa action.”
5
5
  - Execução: provar input inválido, autorização e efeitos com `runAction`, sem rede real.
6
+ - Execução: para `customer.view` com `assistant`, provar por `runtime.readResource` que a
7
+ autorização da action vale e que `context.data` não traz campo fora de `context`.
@@ -8,3 +8,17 @@
8
8
 
9
9
  Teste o binding completo. Um contrato sem handler falha com `testing.no_handler`: os dados do modo
10
10
  design são gerados pelo plugin `opusDesign()` e não são comportamento da action.
11
+
12
+ ## Assistente do recurso
13
+
14
+ `runAction` executa a action, mas não a leitura do recurso. Para uma action com `assistant`
15
+ (ADR 0053), registrar o `ActionDef` com `createRuntime` e chamar
16
+ `runtime.readResource(resource, id, base, { context: true })`:
17
+
18
+ - negar a permissão no `base` e afirmar `ok: false` com a categoria da action, sem o handler rodar;
19
+ - afirmar `resource.title`, `resource.skill` e `context.personalData`;
20
+ - afirmar que `context.data` traz só os caminhos de `context` e que um campo pessoal fora deles não
21
+ aparece;
22
+ - quando a saída puder faltar um caminho declarado, afirmar `omitted` com o motivo `missing`.
23
+
24
+ Os cortes do teto são do SDK e já têm cobertura nele; o teste do produto prova a declaração.
package/src/ai/index.ts CHANGED
@@ -15,3 +15,6 @@ export type { AiAdapter, AiCompleteOptions } from '../core/index.ts'
15
15
  // o formatador. É o ponto de consistência pra qualquer consumidor (chat, copilot, MCP externo).
16
16
  export type { AskOption, AskQuestion, AskAnswer } from '../core/index.ts'
17
17
  export { ASK_USER_TOOL, ASK_TOOL_DESCRIPTION, ASK_INPUT_SCHEMA, formatAnswers } from './ask.ts'
18
+ // Dado do recurso de um assistente (ADR 0053): o bloco delimitado que o host entrega ao modelo.
19
+ export { RESOURCE_DATA_TAG, resourceDataBlock } from './resource-context.ts'
20
+ export type { ResourceDataBlockInput, ResourceDataBlockOptions } from './resource-context.ts'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * O bloco de dado do recurso que o host entrega ao modelo (ADR 0053, seção 3).
3
+ *
4
+ * O dado do recurso é dado, nunca instrução: vai numa mensagem de dado, delimitado, e nunca no
5
+ * prompt de sistema. O bloco leva o tipo, o id, o título e os dados e, quando a projeção cortou ou
6
+ * não encontrou algum campo, `truncated` e `omitted`, para o modelo saber que o dado está
7
+ * incompleto e decidir chamar a action dona. O conteúdo é JSON e nenhum valor consegue fechar o
8
+ * delimitador: os sinais de menor e maior, os sinais que se parecem com eles, os caracteres de
9
+ * controle, os invisíveis (formatação, seletores de variação, preenchimentos) e os espaços
10
+ * incomuns saem como escapes `\uXXXX`, que continuam valendo o mesmo texto quando o JSON é lido.
11
+ */
12
+
13
+ import type { ResourceContext, ResourceDescriptor } from '../core/index.ts'
14
+
15
+ /** A etiqueta padrão do bloco. */
16
+ export const RESOURCE_DATA_TAG = 'resource-data'
17
+
18
+ const TAG_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
19
+
20
+ // Os códigos ficam numéricos para o arquivo não depender da exibição desses sinais.
21
+ //
22
+ // Sinais que um modelo pode ler como `<` ou `>`. A primeira lista sai do confusables.txt do Unicode
23
+ // (UTS #39, versão de 2026-08-06): todo caractere cujo esqueleto contém `<` ou `>`, como ‹, ❮, ≪
24
+ // e ᐸ. A segunda traz colchetes angulares de outros blocos, que o arquivo não liga a `<` e `>` mas
25
+ // que delimitam texto do mesmo jeito. Os que a NFKC transforma em `<` ou `>` são pegos também pela
26
+ // regra de `isUnsafe`, sem depender das listas.
27
+ const ANGLE_CONFUSABLES = [
28
+ 0x02c2, 0x02c3, 0x1032d, 0x1433, 0x1437, 0x1438, 0x1440, 0x1441, 0x1444, 0x1445, 0x150c, 0x16aec,
29
+ 0x16b2, 0x16f3f, 0x1d236, 0x1d237, 0x2039, 0x203a, 0x226a, 0x226b, 0x22d6, 0x22d7, 0x22d8, 0x22d9,
30
+ 0x276e, 0x276f, 0x2a20, 0x2aa5, 0x2cb4, 0x2cb5, 0x2d66, 0xfe64, 0xfe65, 0xff1c, 0xff1e,
31
+ ]
32
+ const ANGLE_BRACKETS = [
33
+ 0x2329, 0x232a, // 〈 〉 técnicos
34
+ 0x276c, 0x276d, 0x2770, 0x2771, // ornamentais
35
+ 0x27e8, 0x27e9, 0x27ea, 0x27eb, // matemáticos
36
+ 0x29fc, 0x29fd, // curvos
37
+ 0x3008, 0x3009, 0x300a, 0x300b, // CJK
38
+ 0xfe3d, 0xfe3e, 0xfe3f, 0xfe40, // formas verticais
39
+ ]
40
+ const ANGLE_LOOKALIKES = new Set([...ANGLE_CONFUSABLES, ...ANGLE_BRACKETS])
41
+
42
+ // Caracteres sem glifo próprio que escondem ou deslocam o texto: preenchimentos do hangul e
43
+ // seletores de variação, que alteram o glifo anterior sem aparecer.
44
+ const INVISIBLE = new Set([0x115f, 0x1160, 0x3164, 0xffa0, 0x180b, 0x180c, 0x180d, 0x180f])
45
+
46
+ function isVariationSelector(code: number): boolean {
47
+ return (code >= 0xfe00 && code <= 0xfe0f) || (code >= 0xe0100 && code <= 0xe01ef)
48
+ }
49
+
50
+ // Controle (C0, DEL, C1 e NEL), formatação invisível (zero-width, bidi, tags), separadores de linha e
51
+ // parágrafo, que o JSON não escapa, e espaços que não são o espaço comum.
52
+ const UNSAFE_CATEGORY = /^[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Zs}]$/u
53
+
54
+ function isUnsafe(char: string): boolean {
55
+ if (char === ' ') return false
56
+ const code = char.codePointAt(0)!
57
+ if (code === 0x3c || code === 0x3e) return true
58
+ if (ANGLE_LOOKALIKES.has(code) || INVISIBLE.has(code) || isVariationSelector(code)) return true
59
+ if (UNSAFE_CATEGORY.test(char)) return true
60
+ if (code < 0x80) return false
61
+ const normalized = char.normalize('NFKC')
62
+ return normalized.includes('<') || normalized.includes('>')
63
+ }
64
+
65
+ function escapeUnsafe(json: string): string {
66
+ let out = ''
67
+ for (const char of json) {
68
+ if (!isUnsafe(char)) {
69
+ out += char
70
+ continue
71
+ }
72
+ for (let index = 0; index < char.length; index += 1) {
73
+ out += `\\u${char.charCodeAt(index).toString(16).padStart(4, '0')}`
74
+ }
75
+ }
76
+ return out
77
+ }
78
+
79
+ export interface ResourceDataBlockOptions {
80
+ /** Etiqueta do delimitador, em minúsculas com hífen. Padrão: `resource-data`. */
81
+ tag?: string
82
+ }
83
+
84
+ /** O que o bloco recebe: o descritor e a projeção de `runtime.readResource(..., { context: true })`. */
85
+ export interface ResourceDataBlockInput {
86
+ resource: Pick<ResourceDescriptor, 'kind' | 'id' | 'title'>
87
+ context: Pick<ResourceContext, 'data' | 'truncated' | 'omitted'>
88
+ }
89
+
90
+ /**
91
+ * Monta o bloco delimitado com o dado do recurso. Exemplo:
92
+ *
93
+ * ```text
94
+ * <resource-data>
95
+ * {"kind":"customer","id":"c1","title":"Maria Souza","data":{"stage":"lead"}}
96
+ * </resource-data>
97
+ * ```
98
+ */
99
+ export function resourceDataBlock(input: ResourceDataBlockInput, options: ResourceDataBlockOptions = {}): string {
100
+ const tag = options.tag ?? RESOURCE_DATA_TAG
101
+ if (!TAG_RE.test(tag)) throw new Error(`A etiqueta "${tag}" precisa estar em minúsculas com hífen.`)
102
+ const { resource, context } = input
103
+ const incomplete = context.truncated || context.omitted.length > 0
104
+ const payload = {
105
+ kind: resource.kind,
106
+ id: resource.id,
107
+ title: resource.title,
108
+ data: context.data,
109
+ ...(incomplete ? { truncated: context.truncated, omitted: context.omitted } : {}),
110
+ }
111
+ const json = escapeUnsafe(JSON.stringify(payload))
112
+ return `<${tag}>\n${json}\n</${tag}>`
113
+ }
@@ -9,8 +9,9 @@
9
9
  * resto server-only) — ver `contracts.ts`. `defineAction` continua válido e
10
10
  * suportado para action server-only, em que não há contrato a compartilhar.
11
11
  *
12
- * Validação estrutural acontece no `Runtime.register()`, não aqui. Esta função
13
- * é puramente sobre **declaração**.
12
+ * A factory valida contradições que existem inteiras na declaração, como um
13
+ * `grouping` que referencia um filtro incompatível. O runtime repete essas
14
+ * guardas para actions montadas sem a factory.
14
15
  *
15
16
  * Ver §2 do protocolo.
16
17
  */
@@ -22,6 +23,7 @@ import type {
22
23
  SimpleAction,
23
24
  ViewAction,
24
25
  } from './types.ts'
26
+ import { assertValidListGrouping } from './list-grouping.ts'
25
27
 
26
28
  /* eslint-disable @typescript-eslint/no-explicit-any */
27
29
 
@@ -61,6 +63,7 @@ export function defineAction<In, Out, ParsedIn = In>(
61
63
  ): ViewAction<In, Out, ParsedIn>
62
64
 
63
65
  export function defineAction(spec: any): any {
66
+ if (spec.kind === 'list') assertValidListGrouping(spec)
64
67
  return spec
65
68
  }
66
69