@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
|
@@ -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.
|
|
32
|
-
|
|
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
|
|
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.
|
|
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
|
+
}
|