@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
package/CHANGELOG.md CHANGED
@@ -7,6 +7,99 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
7
7
  `opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
8
8
  mudança cobra do seu código.
9
9
 
10
+ ## 25.2.0 — 2026-10-03
11
+
12
+ Uma action `view` pode declarar o assistente do recurso que ela lê (ADR 0053). Com isso, a conversa
13
+ especializada num cliente, num relatório ou num pedido passa a ler o recurso, conferir o acesso,
14
+ saber o procedimento da IA e escolher o dado ao modelo a partir de uma declaração só, no contrato:
15
+
16
+ ```ts
17
+ ai: true,
18
+ assistant: {
19
+ resource: 'customer',
20
+ skill: 'review-customer',
21
+ label: 'Assistente do cliente',
22
+ title: 'name',
23
+ context: ['kind', 'stage', 'leads.totalCount'],
24
+ },
25
+ ```
26
+
27
+ A action que declara passa a ser a action dona daquele tipo de recurso. Ela precisa ser `view`,
28
+ estar publicada para a IA, declarar `personalData` e receber só `{ id }`.
29
+
30
+ - `runtime.readResource(resource, id, base, { context })` executa a action dona pelo mesmo
31
+ `execute` de qualquer chamada, com a autorização, a auditoria e a marca de dado pessoal dela, e
32
+ devolve o descritor (`kind`, `id`, `title`, `label`, `skill`) e, com `context: true`, a projeção
33
+ limitada do dado. Um tipo sem dona devolve `runtime.assistant_resource_unknown` sem executar nada.
34
+ - `runtime.assistants()` lista as declarações com a action dona, para o produto montar rotas sem
35
+ lista paralela e conferir num teste que cada `skill` existe no registro dele.
36
+ - `projectAssistantContext(assistant, output)` devolve só os caminhos declarados e, acima do teto
37
+ (4.000 caracteres por padrão, de 1.000 a 16.000 em `maxContextChars`), corta por etapas até
38
+ caber, marcando `truncated` e listando o que saiu em `omitted`. Caminho ausente na saída é
39
+ omitido com o motivo `missing` e gera aviso no log, sem falhar a leitura.
40
+ - `resourceDataBlock`, de `@softize/opus/ai`, monta o bloco delimitado que o host entrega ao modelo
41
+ numa mensagem de dado, com os escapes que impedem um valor de fechar o delimitador.
42
+ - `runtime.aiTools()` leva `metadata.assistant`, e o `ListTools` do MCP publica
43
+ `com.softize.opus/assistant` em `_meta`.
44
+ - O registro recusa a declaração inválida (`runtime.assistant_declaration_invalid`), o teto fora do
45
+ intervalo (`runtime.assistant_context_limit_invalid`) e duas donas do mesmo recurso
46
+ (`runtime.assistant_duplicate_resource`). O `opus check` ganha a regra `assistant-declaration`, o
47
+ `opus gen` confere `title`, `context` e a entrada contra os schemas, e o `opus copy` inventaria
48
+ `assistant.label`.
49
+ - `@softize/opus/assistant-schema`, com tipos, exporta `assistantContextLeaves(output, context)`,
50
+ que lista, a partir do JSON Schema da saída no manifest, os campos folha que cada caminho de
51
+ `context` leva ao modelo. Um caminho que termina num objeto ou numa lista leva tudo abaixo dele; use a função num
52
+ teste para garantir que nenhum campo pessoal chega ao modelo por esse caminho.
53
+
54
+ O manifest passa a projetar, em toda action, a publicação para a IA em `ai` (`enabled`,
55
+ `description`, `destructive`, `requiresConfirmation`) e, na action dona, `assistant`. Se o seu
56
+ projeto versiona o manifest, rode `opus gen` depois da atualização: sem isso, a conferência de
57
+ frescor do manifest acusa a diferença. Nada muda para quem não declara assistente.
58
+
59
+ ## 25.1.0 — 2026-10-03
60
+
61
+ `FloatingLayer`, de `@softize/opus/ui/react`, é uma camada que continua utilizável por cima de
62
+ qualquer `Dialog` ou `Drawer` modal, como o botão e o painel de um assistente (ADR 0052). Quem está
63
+ preenchendo um formulário em um modal pode usar a camada sem fechar o modal nem perder o que digitou.
64
+ Os filhos vão para um hospedeiro fixo no `body`, `div[data-slot="floating-layer"]`, e cada um se
65
+ posiciona contra a janela. Com um `Dialog` ou `Drawer` aberto:
66
+
67
+ - clicar na camada não fecha o modal, mesmo que o elemento clicado suma no próprio clique, e o véu
68
+ continua fechando, respeitando `closeDisabled`;
69
+ - o foco pode ir para a camada por clique ou por código sem ser puxado de volta, e o Tab passa do
70
+ último elemento do modal para a camada e dela de volta ao primeiro; quando o elemento focado da
71
+ camada some, o foco volta ao campo do modal em que a pessoa estava, e um modal que fecha com a
72
+ pessoa na camada não leva o foco ao gatilho dele;
73
+ - o Esc age onde a pessoa está: na camada, nenhum modal fecha e `onEscapeKeyDown` é chamado uma vez
74
+ por tecla; no modal, o modal fecha;
75
+ - a camada rola e continua visível ao leitor de tela, enquanto a página atrás segue travada e
76
+ escondida.
77
+
78
+ O hospedeiro publica o `Dialog` ou `Drawer` do topo em `data-modal` (`none`, `dialog` ou `drawer`) e o
79
+ lado do Drawer em `data-modal-side`, para o consumidor tirar o painel de cima do modal. O Opus não
80
+ desloca nem redimensiona modais. Os patterns construídos sobre `Dialog` e `Drawer`, como
81
+ `ActionFormDialog`, `ActionListDialog`, `Presentation` e `CommandDialog`, convivem com a camada sem
82
+ mudança.
83
+
84
+ Outras mudanças:
85
+
86
+ - Popover, Menu, Tooltip, Select, Dialog e Drawer abertos de dentro de uma `FloatingLayer` vão para o
87
+ hospedeiro dela, e não para o `body`; fora da camada, nada muda;
88
+ - `Dialog mode="alert"`, que também serve às confirmações do `DialogHost`, passa à faixa `z-70`,
89
+ acima da camada (`z-60`) e dos demais modais (`z-50`), e cobre a camada até a resposta, inclusive
90
+ enquanto um `Dialog` aberto de dentro dele estiver aberto. Popover, Menu e Tooltip do Opus abertos
91
+ de dentro de um alerta acompanham a faixa dele. Dois efeitos para conferir no seu código: um
92
+ elemento próprio com `z-index` entre 51 e 69, que antes ficava acima do alerta, agora fica abaixo
93
+ dele; e uma sobreposição de outra biblioteca com `z-50`, aberta de dentro de um alerta, fica
94
+ abaixo do véu dele. Durante a animação de saída, o alerta deixa de receber clique. A escala está
95
+ em `docs/stacking-scale.md`;
96
+ - `DialogPortal` e `MenuPortal` sem `container` usam o hospedeiro da camada quando abertos de
97
+ dentro dela;
98
+ - a `ref` de `DialogContent` e `DrawerContent` continua recebendo a limpeza das callback refs do
99
+ React 19, agora composta com a ref interna da camada;
100
+ - o pacote ganha testes em navegador real (Chromium, pelo modo navegador do Vitest com Playwright)
101
+ para o foco da camada: `pnpm test:browser` roda à parte do `pnpm test` e entra no gate de release.
102
+
10
103
  ## 25.0.0 — 2026-10-02
11
104
 
12
105
  A exclusão lógica passa a declarar quando o registro excluído sai de vez (ADR 0051). `softDelete`
package/bin/cli.mjs CHANGED
@@ -426,6 +426,11 @@ Regras das actions:
426
426
  binding (o join contrato↔bind é cross-file)
427
427
  ignored-by-kind — \`emits\` ou \`background\` em action list/view: o runtime
428
428
  e o manifest só os aplicam em simple e form
429
+ assistant-declaration — o assistente do recurso (ADR 0053): só em view, com \`ai\`
430
+ habilitado, \`personalData\` declarado, \`resource\` em camelCase,
431
+ \`skill\` em minúsculas com hífen, \`label\`, \`title\` e
432
+ \`context\` presentes e \`maxContextChars\`, quando literal,
433
+ de 1.000 a 16.000; o mesmo \`resource\` em duas actions reprova
429
434
 
430
435
  Regras da UI (@softize/opus/ui):
431
436
  ui-structure — anatomia pública das superfícies compostas (ADR 0005):
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tipos de `@softize/opus/assistant-schema` (ADR 0053): a conferência do assistente do recurso
3
+ * contra o JSON Schema da action, como o manifest o traz. A implementação é `assistant-schema.mjs`.
4
+ */
5
+
6
+ /** Um JSON Schema como o `opus gen` o projeta no manifest. */
7
+ export type JsonSchema = Record<string, unknown>
8
+
9
+ /** Os campos que um caminho de `context` leva ao modelo. */
10
+ export interface AssistantContextLeaves {
11
+ /** O caminho declarado em `context`. */
12
+ path: string
13
+ /** Os campos folha alcançados, com ponto e sem índice de lista; `*` marca a chave de um mapa. */
14
+ leaves: string[]
15
+ /**
16
+ * Pontos em que o schema não declara forma ou volta a si mesmo por `$ref`: qualquer campo abaixo
17
+ * deles pode ir ao modelo, e a conferência não alcança.
18
+ */
19
+ open: string[]
20
+ }
21
+
22
+ /**
23
+ * Os campos folha que cada caminho de `context` leva ao modelo, a partir do JSON Schema da saída.
24
+ * Serve a um teste do consumidor que reprova campo pessoal alcançado por um caminho que termina
25
+ * num objeto ou numa lista.
26
+ */
27
+ export function assistantContextLeaves(
28
+ output: JsonSchema,
29
+ context: readonly string[],
30
+ root?: JsonSchema,
31
+ ): AssistantContextLeaves[]
32
+
33
+ /** O caminho existe em algum ramo não nulo do schema; listas são atravessadas. */
34
+ export function schemaHasPath(schema: JsonSchema, segments: readonly string[], root?: JsonSchema): boolean
35
+
36
+ /** O título resolve para texto ou número, fora de lista, em algum ramo não nulo. */
37
+ export function schemaTitleIsText(schema: JsonSchema, segments: readonly string[], root?: JsonSchema): boolean
38
+
39
+ /**
40
+ * Os problemas do assistente diante dos schemas de entrada e de saída, como frases. Lista vazia
41
+ * quando a declaração confere.
42
+ */
43
+ export function assistantSchemaIssues(args: {
44
+ assistant: { title?: unknown; context?: unknown }
45
+ input: unknown
46
+ output: unknown
47
+ }): string[]
48
+
49
+ /** O `ai` de uma action normalizado como o manifest o projeta. */
50
+ export function serializeAiConfig(ai: unknown): {
51
+ enabled: boolean
52
+ description: string | null
53
+ destructive: boolean
54
+ requiresConfirmation: boolean
55
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * assistant-schema — confere o assistente do recurso (ADR 0053) contra o JSON Schema da action,
3
+ * no `opus gen`: `title` e cada caminho de `context` precisam existir na saída, e a entrada precisa
4
+ * ser um objeto em que `id`, do tipo texto, é o único campo obrigatório.
5
+ *
6
+ * Num campo anulável ou numa união (`anyOf`, `oneOf`, `allOf`), o caminho vale se existir em algum
7
+ * ramo que não seja `null`. Uma lista é atravessada: o caminho continua nos itens. Um schema sem
8
+ * forma declarada (`{}`, de `z.unknown()` ou `z.any()`) aceita qualquer caminho abaixo dele, porque
9
+ * não há o que conferir.
10
+ */
11
+
12
+ const COMBINATORS = ["anyOf", "oneOf", "allOf"];
13
+ const STRUCTURE_KEYS = ["type", "properties", "items", "additionalProperties", "enum", "const"];
14
+ const MAX_DEPTH = 64;
15
+ // Segmentos que mexem no protótipo de um objeto JavaScript; o registro do runtime também os recusa.
16
+ const RESERVED_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
17
+
18
+ function reservedSegments(path) {
19
+ return typeof path === "string" ? path.split(".").filter((segment) => RESERVED_SEGMENTS.has(segment)) : [];
20
+ }
21
+
22
+ function resolveRef(root, ref) {
23
+ if (!ref.startsWith("#/")) return undefined;
24
+ let node = root;
25
+ for (const raw of ref.slice(2).split("/")) {
26
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
27
+ if (node === null || typeof node !== "object" || !Object.hasOwn(node, key)) return undefined;
28
+ node = node[key];
29
+ }
30
+ return node;
31
+ }
32
+
33
+ function isNullOnly(schema) {
34
+ if (schema.type === "null") return true;
35
+ if (Array.isArray(schema.type) && schema.type.every((type) => type === "null")) return true;
36
+ // O alvo OpenAPI 3.0 do zod-to-json-schema escreve o ramo nulo como `{ enum: ['null'], nullable: true }`.
37
+ return (
38
+ Array.isArray(schema.enum) &&
39
+ schema.enum.length > 0 &&
40
+ schema.enum.every((value) => value === null || value === "null") &&
41
+ (schema.nullable === true || schema.enum.includes(null))
42
+ );
43
+ }
44
+
45
+ const NO_REFS = new Set();
46
+ /** Marca o ponto em que um `$ref` volta a um schema que já está no caminho atual. */
47
+ const CYCLE = Object.freeze({ cycle: true });
48
+
49
+ /**
50
+ * Os ramos concretos e não nulos de um schema, com `$ref` e combinadores expandidos. Cada ramo traz
51
+ * os `$ref` percorridos no caminho atual; um `$ref` que se repete ali vira `CYCLE`, em vez de
52
+ * expandir de novo.
53
+ */
54
+ function expand(schema, root, active = NO_REFS, depth = 0) {
55
+ if (schema === null || typeof schema !== "object" || depth > MAX_DEPTH) return [];
56
+ if (typeof schema.$ref === "string") {
57
+ if (active.has(schema.$ref)) return [{ schema: CYCLE, active }];
58
+ const target = resolveRef(root, schema.$ref);
59
+ return target === undefined ? [] : expand(target, root, new Set([...active, schema.$ref]), depth + 1);
60
+ }
61
+ const nested = COMBINATORS.flatMap((key) =>
62
+ Array.isArray(schema[key]) ? schema[key].flatMap((branch) => expand(branch, root, active, depth + 1)) : [],
63
+ );
64
+ const hasOwnStructure = STRUCTURE_KEYS.some((key) => Object.hasOwn(schema, key));
65
+ const combined = COMBINATORS.some((key) => Array.isArray(schema[key]));
66
+ if (combined && !hasOwnStructure) return nested;
67
+ if (isNullOnly(schema)) return nested;
68
+ return [{ schema, active }, ...nested];
69
+ }
70
+
71
+ /** Os ramos sem os ciclos, para quem só olha o tipo do próprio nó. */
72
+ function branches(schema, root) {
73
+ return expand(schema, root).flatMap(({ schema: branch }) => (branch === CYCLE ? [] : [branch]));
74
+ }
75
+
76
+ function isOpen(schema) {
77
+ return (
78
+ !Object.hasOwn(schema, "$ref") &&
79
+ !STRUCTURE_KEYS.some((key) => Object.hasOwn(schema, key)) &&
80
+ !COMBINATORS.some((key) => Object.hasOwn(schema, key))
81
+ );
82
+ }
83
+
84
+ function isArraySchema(schema) {
85
+ return schema.type === "array" || (Array.isArray(schema.type) && schema.type.includes("array")) || Object.hasOwn(schema, "items");
86
+ }
87
+
88
+ /**
89
+ * Os schemas alcançados por `segments` em todos os ramos não nulos. Um schema sem forma alcança
90
+ * qualquer caminho abaixo dele e entra como está. Com `throughLists`, uma lista é atravessada. Um
91
+ * ciclo de `$ref` sem avançar no caminho (por lista ou união) não leva a lugar nenhum; ao descer
92
+ * por um campo, o caminho avança, e o mesmo `$ref` pode voltar, como numa árvore.
93
+ */
94
+ function collectTargets(schema, segments, root, throughLists, out, active = NO_REFS, depth = 0) {
95
+ if (depth > MAX_DEPTH) return;
96
+ if (segments.length === 0) {
97
+ out.push(schema);
98
+ return;
99
+ }
100
+ for (const { schema: branch, active: refs } of expand(schema, root, active)) {
101
+ if (branch === CYCLE) continue;
102
+ if (isOpen(branch)) {
103
+ out.push(branch);
104
+ continue;
105
+ }
106
+ if (isArraySchema(branch)) {
107
+ if (!throughLists) continue;
108
+ const items = Array.isArray(branch.items) ? branch.items : [branch.items];
109
+ for (const item of items) {
110
+ if (item !== undefined) collectTargets(item, segments, root, throughLists, out, refs, depth + 1);
111
+ }
112
+ continue;
113
+ }
114
+ const [head, ...rest] = segments;
115
+ const properties = branch.properties;
116
+ if (properties !== null && typeof properties === "object" && Object.hasOwn(properties, head)) {
117
+ collectTargets(properties[head], rest, root, throughLists, out, NO_REFS, depth + 1);
118
+ }
119
+ const additional = branch.additionalProperties;
120
+ if (additional !== null && typeof additional === "object") {
121
+ collectTargets(additional, rest, root, throughLists, out, NO_REFS, depth + 1);
122
+ }
123
+ }
124
+ }
125
+
126
+ /** O caminho `segments` existe em algum ramo não nulo de `schema`. */
127
+ export function schemaHasPath(schema, segments, root = schema) {
128
+ const targets = [];
129
+ collectTargets(schema, segments, root, true, targets);
130
+ return targets.length > 0;
131
+ }
132
+
133
+ const TITLE_TYPES = new Set(["string", "number", "integer"]);
134
+
135
+ /**
136
+ * O título resolve para texto ou número em algum ramo não nulo, sem atravessar lista: é o que o
137
+ * runtime consegue mostrar como nome do recurso.
138
+ */
139
+ export function schemaTitleIsText(schema, segments, root = schema) {
140
+ const targets = [];
141
+ collectTargets(schema, segments, root, false, targets);
142
+ return targets.some((target) =>
143
+ isOpen(target) ||
144
+ branches(target, root).some((branch) => {
145
+ if (isOpen(branch)) return true;
146
+ const types = Array.isArray(branch.type) ? branch.type : [branch.type];
147
+ return types.some((type) => TITLE_TYPES.has(type));
148
+ }),
149
+ );
150
+ }
151
+
152
+ /**
153
+ * As folhas abaixo de `schema`. Um `$ref` que volta a um schema já no caminho atual (uma estrutura
154
+ * recursiva) entra em `open` naquele ponto: abaixo dele, qualquer profundidade pode chegar ao modelo.
155
+ */
156
+ function collectLeaves(schema, prefix, root, leaves, open, active = NO_REFS, depth = 0) {
157
+ if (depth > MAX_DEPTH) {
158
+ open.add(prefix);
159
+ return;
160
+ }
161
+ for (const { schema: branch, active: refs } of expand(schema, root, active)) {
162
+ if (branch === CYCLE || isOpen(branch)) {
163
+ open.add(prefix);
164
+ continue;
165
+ }
166
+ if (isArraySchema(branch)) {
167
+ const items = Array.isArray(branch.items) ? branch.items : [branch.items];
168
+ for (const item of items) if (item !== undefined) collectLeaves(item, prefix, root, leaves, open, refs, depth + 1);
169
+ continue;
170
+ }
171
+ const properties = branch.properties;
172
+ const hasProperties = properties !== null && typeof properties === "object" && Object.keys(properties).length > 0;
173
+ if (hasProperties) {
174
+ for (const [key, child] of Object.entries(properties)) {
175
+ collectLeaves(child, `${prefix}.${key}`, root, leaves, open, refs, depth + 1);
176
+ }
177
+ }
178
+ const additional = branch.additionalProperties;
179
+ if (additional !== null && typeof additional === "object") {
180
+ collectLeaves(additional, `${prefix}.*`, root, leaves, open, refs, depth + 1);
181
+ }
182
+ if (!hasProperties && !(additional !== null && typeof additional === "object")) leaves.add(prefix);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Os campos folha que cada caminho de `context` leva ao modelo, segundo o JSON Schema da saída
188
+ * (ADR 0053). Um caminho que termina num objeto ou numa lista projeta tudo abaixo dele; esta função
189
+ * lista o que é, para o consumidor conferir num teste, por exemplo, que nenhum campo pessoal chega
190
+ * ao modelo por um caminho que termina num contêiner.
191
+ *
192
+ * - `leaves`: caminhos das folhas, com ponto e sem índice de lista; `*` marca a chave de um mapa;
193
+ * - `open`: pontos em que o schema não declara forma (`z.unknown()`, `z.any()`) ou volta a si mesmo
194
+ * por `$ref`: qualquer campo abaixo deles pode ir ao modelo, e a conferência não alcança.
195
+ */
196
+ export function assistantContextLeaves(output, context, root = output) {
197
+ return context.map((path) => {
198
+ const targets = [];
199
+ collectTargets(output, path.split("."), root, true, targets);
200
+ const leaves = new Set();
201
+ const open = new Set();
202
+ for (const target of targets) collectLeaves(target, path, root, leaves, open);
203
+ return { path, leaves: [...leaves].sort(), open: [...open].sort() };
204
+ });
205
+ }
206
+
207
+ function isText(schema, root) {
208
+ return branches(schema, root).some(
209
+ (branch) => branch.type === "string" || (Array.isArray(branch.type) && branch.type.includes("string")),
210
+ );
211
+ }
212
+
213
+ function unserializable(schema) {
214
+ return schema === null || schema === undefined || schema.__unserializable === true;
215
+ }
216
+
217
+ /**
218
+ * Os problemas do assistente de uma action diante dos JSON Schemas de entrada e de saída, como
219
+ * frases. Lista vazia quando a declaração cabe nos schemas.
220
+ */
221
+ export function assistantSchemaIssues({ assistant, input, output }) {
222
+ const issues = [];
223
+ if (unserializable(input)) {
224
+ issues.push("o schema de entrada não pôde ser convertido em JSON Schema para conferir o `id`");
225
+ } else {
226
+ const objects = branches(input, input).filter(
227
+ (branch) => branch.type === "object" || Object.hasOwn(branch, "properties"),
228
+ );
229
+ if (objects.length !== 1) {
230
+ issues.push("a entrada precisa ser um objeto com `id`, do tipo texto, como único campo obrigatório");
231
+ } else {
232
+ const [object] = objects;
233
+ const required = Array.isArray(object.required) ? object.required : [];
234
+ const id = object.properties?.id;
235
+ if (id === undefined || !required.includes("id")) {
236
+ issues.push("a entrada precisa ter `id` obrigatório");
237
+ } else if (!isText(id, input)) {
238
+ issues.push("o `id` da entrada precisa ser do tipo texto");
239
+ }
240
+ const others = required.filter((key) => key !== "id");
241
+ if (others.length > 0) {
242
+ issues.push(
243
+ `a entrada só pode exigir \`id\`, mas também exige ${others.map((key) => `\`${key}\``).join(", ")}; dê um valor padrão a esses campos ou torne-os opcionais`,
244
+ );
245
+ }
246
+ }
247
+ }
248
+ const reserved = [assistant.title, ...(Array.isArray(assistant.context) ? assistant.context : [])].filter(
249
+ (path) => reservedSegments(path).length > 0,
250
+ );
251
+ if (reserved.length > 0) {
252
+ issues.push(
253
+ `${reserved.map((path) => `\`${path}\``).join(", ")} usa __proto__, constructor ou prototype, que não são campos da saída`,
254
+ );
255
+ return issues;
256
+ }
257
+ if (unserializable(output)) {
258
+ issues.push("o schema de saída não pôde ser convertido em JSON Schema para conferir `title` e `context`");
259
+ return issues;
260
+ }
261
+ const missing = (path) => typeof path !== "string" || !schemaHasPath(output, path.split("."));
262
+ if (missing(assistant.title)) {
263
+ issues.push(`\`${assistant.title}\`, de \`title\`, não existe na saída`);
264
+ } else if (!schemaTitleIsText(output, assistant.title.split("."))) {
265
+ issues.push(
266
+ `\`${assistant.title}\`, de \`title\`, precisa ser texto ou número, fora de lista, para nomear o recurso`,
267
+ );
268
+ }
269
+ const absent = (Array.isArray(assistant.context) ? assistant.context : []).filter(missing);
270
+ if (absent.length > 0) {
271
+ const list = absent.map((path) => `\`${path}\``).join(", ");
272
+ issues.push(`${list}, de \`context\`, ${absent.length === 1 ? "não existe" : "não existem"} na saída`);
273
+ }
274
+ return issues;
275
+ }
276
+
277
+ /** O `ai` da action normalizado para o manifest. */
278
+ export function serializeAiConfig(ai) {
279
+ const config = ai === true ? { enabled: true } : ai !== null && typeof ai === "object" ? ai : null;
280
+ return {
281
+ enabled: config !== null && config.enabled !== false,
282
+ description: typeof config?.description === "string" && config.description.length > 0 ? config.description : null,
283
+ destructive: config?.destructive === true,
284
+ requiresConfirmation: config?.requiresConfirmation === true,
285
+ };
286
+ }