@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +93 -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 +3 -0
  8. package/bin/lib/gen-runner.mjs +29 -1
  9. package/bin/lib/mcp.mjs +1 -1
  10. package/docs/adr/0052-floating-layer-coexists-with-modal-surfaces.md +668 -0
  11. package/docs/adr/0053-read-action-declares-the-resource-assistant.md +443 -0
  12. package/docs/protocol.md +54 -0
  13. package/docs/releasing.md +8 -2
  14. package/docs/stacking-scale.md +53 -0
  15. package/package.json +10 -1
  16. package/registry/skills/create-opus-action/SKILL.md +8 -3
  17. package/registry/skills/create-opus-action/references/contract-and-binding.md +24 -1
  18. package/registry/skills/create-opus-action/references/evaluations.md +3 -0
  19. package/registry/skills/test-opus-action/SKILL.md +4 -1
  20. package/registry/skills/test-opus-action/references/evaluations.md +2 -0
  21. package/registry/skills/test-opus-action/references/harness.md +14 -0
  22. package/src/ai/index.ts +3 -0
  23. package/src/ai/resource-context.ts +113 -0
  24. package/src/core/assistant.ts +545 -0
  25. package/src/core/index.ts +23 -0
  26. package/src/core/runtime.ts +136 -0
  27. package/src/core/types.ts +34 -0
  28. package/src/mcp/index.ts +3 -0
  29. package/src/ui/components/primitives/dialog.tsx +285 -26
  30. package/src/ui/components/primitives/drawer.tsx +104 -13
  31. package/src/ui/components/primitives/floating-layer.tsx +79 -0
  32. package/src/ui/components/primitives/menu.tsx +17 -2
  33. package/src/ui/components/primitives/popover.tsx +6 -1
  34. package/src/ui/components/primitives/tooltip.tsx +6 -1
  35. package/src/ui/docs/content/actions.md +97 -1
  36. package/src/ui/docs/content/ai.md +4 -2
  37. package/src/ui/docs/content/cli.md +2 -1
  38. package/src/ui/docs/content/dialog.md +3 -0
  39. package/src/ui/docs/content/drawer.md +3 -0
  40. package/src/ui/docs/content/floating-layer.md +178 -0
  41. package/src/ui/docs/content/mcp.md +3 -1
  42. package/src/ui/docs/registry.tsx +6 -0
  43. package/src/ui/lib/dismiss-on-iframe.ts +4 -1
  44. package/src/ui/lib/modal-layers.ts +547 -0
  45. package/src/ui/meta.ts +6 -0
  46. 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. 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
+ }