@softize/opus 22.0.0 → 22.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 +59 -0
- package/bin/cli.mjs +18 -1
- package/bin/lib/check.mjs +28 -2
- package/bin/lib/gen-manifest.mjs +2 -0
- package/bin/lib/gen-runner.mjs +5 -0
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +117 -0
- package/docs/adr/0041-actions-declare-effect-and-personal-data.md +139 -0
- package/docs/protocol.md +15 -2
- package/package.json +5 -1
- package/registry/skills/create-opus-action/SKILL.md +7 -2
- package/registry/skills/create-opus-action/references/contract-and-binding.md +11 -4
- package/registry/skills/create-opus-action/references/evaluations.md +3 -0
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +6 -4
- package/registry/skills/test-opus-action/SKILL.md +2 -2
- package/registry/skills/test-opus-action/references/harness.md +2 -2
- package/registry/templates/app/src/App.tsx +1 -1
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +2 -19
- package/registry/templates/app/src/domains/tasks/actions/list.ts +5 -5
- package/registry/templates/app/vite.config.ts +3 -2
- package/src/core/action-nature.ts +40 -0
- package/src/core/audit.ts +12 -4
- package/src/core/domain.ts +2 -2
- package/src/core/index.ts +2 -0
- package/src/core/runtime.ts +56 -1
- package/src/core/types.ts +46 -20
- package/src/design/index.ts +179 -0
- package/src/mcp/index.ts +6 -0
- package/src/testing/fake.ts +2 -2
- package/src/testing/index.ts +7 -3
- package/src/ui/docs/content/actions.md +79 -44
- package/src/ui/docs/content/mcp.md +6 -0
- package/src/ui/docs/content/testing.md +9 -25
- package/src/vite/design.ts +133 -23
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,65 @@ 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
|
+
## 22.2.0 — 2026-09-29
|
|
11
|
+
|
|
12
|
+
As actions passam a declarar o que fazem com os dados (ADR 0041). Dois campos novos no contrato,
|
|
13
|
+
ambos opcionais nesta versão:
|
|
14
|
+
|
|
15
|
+
- **`effect: 'read' | 'write'`** diz se a action só lê ou altera estado. `list` e `view` já são
|
|
16
|
+
leitura, porque são servidas por `GET`, e não declaram; declarar `effect: 'write'` nelas é recusado
|
|
17
|
+
ao registrar a action (`runtime.invalid_action_effect`) e apontado pelo `opus check`. `simple` e
|
|
18
|
+
`form` declaram.
|
|
19
|
+
- **`personalData: boolean`** diz se a resposta contém dados que identificam pessoas.
|
|
20
|
+
|
|
21
|
+
Com os dois declarados, o runtime:
|
|
22
|
+
|
|
23
|
+
- marca no `AuditRecord` toda execução bem-sucedida de action com `personalData: true`
|
|
24
|
+
(`personalData: { records }`, com a quantidade devolvida) e registra `effect`, qualquer que seja o
|
|
25
|
+
consumidor. Com `audit: false`, a exposição ainda gera um registro mínimo, sem entrada nem saída
|
|
26
|
+
(`meta.exposureOnly`). Execução com erro, enfileiramento em segundo plano e resposta simulada do
|
|
27
|
+
modo design não são marcados;
|
|
28
|
+
- acrescenta `opus.action.effect` e `opus.action.personal_data` aos atributos do span;
|
|
29
|
+
- projeta `effect` e `personalData` no manifest;
|
|
30
|
+
- publica nas ferramentas de IA (`runtime.aiTools()`) e no `ListTools` do MCP as anotações
|
|
31
|
+
`readOnlyHint` e `destructiveHint` (numa escrita, sempre explícito, porque o MCP trata a ausência
|
|
32
|
+
como destrutiva), e em `_meta` os campos `com.softize.opus/effect` e
|
|
33
|
+
`com.softize.opus/personal-data`.
|
|
34
|
+
|
|
35
|
+
O `opus check` conta, numa linha de aviso que não reprova, quantas actions `simple` ou `form` não
|
|
36
|
+
declaram `effect` e quantas actions não declaram `personalData`.
|
|
37
|
+
|
|
38
|
+
**O que fazer:** rode `opus gen` depois do bump. O manifest passa a ter `effect` e `personalData` em
|
|
39
|
+
toda action (`null` quando não declarado), e sem regenerar o `manifest:check` falha mesmo sem mudança
|
|
40
|
+
de código. O resto é gradual: declare `effect` nas actions `simple` e `form` e `personalData` em
|
|
41
|
+
todas; uma major futura passará a exigir as duas declarações. Um sink de audit que descarta leituras
|
|
42
|
+
(`actionKind` `list` ou `view`) deve preservar as que trazem `personalData`, se a aplicação precisar
|
|
43
|
+
registrar a exposição.
|
|
44
|
+
|
|
45
|
+
## 22.1.0 — 2026-09-28
|
|
46
|
+
|
|
47
|
+
Os dados do modo design saem do contrato (ADR 0037). No modo design, toda action passa a responder
|
|
48
|
+
sem que o contrato declare nada: a resposta é gerada pelo `fake` a partir do `output`, a mesma
|
|
49
|
+
requisição recebe a mesma resposta, uma `list` devolve três páginas do tamanho pedido em `limit` (20
|
|
50
|
+
por padrão) pela paginação declarada, e a saída repete o `id` da entrada quando o schema aceita.
|
|
51
|
+
Quando o design precisa de coerência, um cenário escrito com `defineDesignScenario`, do novo
|
|
52
|
+
`@softize/opus/design`, responde no lugar da resposta gerada. O plugin encontra os cenários pela
|
|
53
|
+
opção `scenarios` de `opusDesign()`, com caminhos ou globs; um cenário de action inexistente, ou
|
|
54
|
+
dois cenários para a mesma action, impedem o runtime de design de subir, e o erro lista os nomes.
|
|
55
|
+
|
|
56
|
+
`mockHandler` fica obsoleto. Ele ainda responde no modo design, abaixo do cenário e acima da
|
|
57
|
+
resposta gerada, e sai na 23.0.0. Os tipos o marcam com `@deprecated`. O `opus check` passa a
|
|
58
|
+
contar, numa linha de aviso que não reprova, quantas actions ainda o declaram. A sonda
|
|
59
|
+
`/__opus/design` ganha `scenarios` e `generated`; `mocked` e `unmocked` mantêm o formato anterior.
|
|
60
|
+
|
|
61
|
+
O template de `opus create` e as skills `create-opus-action` e `test-opus-action` deixam de ensinar
|
|
62
|
+
`mockHandler`.
|
|
63
|
+
|
|
64
|
+
**O que fazer:** nada é obrigatório nesta versão. Para migrar, apague os `mockHandler` dos
|
|
65
|
+
contratos; se a resposta de algum importar para o design, transforme-a em cenário e aponte a opção
|
|
66
|
+
`scenarios` do plugin para o lugar onde os cenários ficam. Testes que usavam `runAction` com
|
|
67
|
+
`OPUS_MODE=design` para exercitar o mock devem testar a action ligada ao handler.
|
|
68
|
+
|
|
10
69
|
## 22.0.0 — 2026-09-28
|
|
11
70
|
|
|
12
71
|
As superfícies passam a usar uma introdução única e ganham composições explícitas para conteúdo
|
package/bin/cli.mjs
CHANGED
|
@@ -40,7 +40,24 @@ async function cmdCheck(dir) {
|
|
|
40
40
|
for (const error of materialization.errors) log('error', `✗ opus check: ${error}`)
|
|
41
41
|
process.exit(1)
|
|
42
42
|
}
|
|
43
|
-
const { files, actions, contracts, findings, hasMarker } =
|
|
43
|
+
const { files, actions, contracts, deprecatedMockHandlers, missingEffect, missingPersonalData, findings, hasMarker } =
|
|
44
|
+
await scanDir(root)
|
|
45
|
+
// Aviso de migração, não violação: uma linha só, para não poluir o gate a cada execução.
|
|
46
|
+
if (deprecatedMockHandlers > 0) {
|
|
47
|
+
log(
|
|
48
|
+
'warn',
|
|
49
|
+
`⚠ opus check: ${deprecatedMockHandlers} action(s) ainda declaram \`mockHandler\`, que sai na próxima major. ` +
|
|
50
|
+
'No modo design, a action sem mock já responde com dado gerado; use `defineDesignScenario` quando o design precisar de um cenário (ADR 0037).',
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (missingEffect > 0 || missingPersonalData > 0) {
|
|
55
|
+
log(
|
|
56
|
+
'warn',
|
|
57
|
+
`⚠ opus check: ${missingEffect} action(s) simple/form sem \`effect\` e ${missingPersonalData} sem \`personalData\`. ` +
|
|
58
|
+
'Declare se a action lê ou escreve e se devolve dado pessoal; uma major futura passará a exigir as duas declarações (ADR 0041).',
|
|
59
|
+
)
|
|
60
|
+
}
|
|
44
61
|
|
|
45
62
|
if (actions === 0 && findings.length === 0) {
|
|
46
63
|
const verdict = emptyScanVerdict({ hasMarker, contracts })
|
package/bin/lib/check.mjs
CHANGED
|
@@ -51,6 +51,8 @@ const FIELD_GROUP = {
|
|
|
51
51
|
errors: 1,
|
|
52
52
|
input: 2,
|
|
53
53
|
output: 2,
|
|
54
|
+
effect: 2,
|
|
55
|
+
personalData: 2,
|
|
54
56
|
public: 3,
|
|
55
57
|
requires: 3,
|
|
56
58
|
authorize: 3,
|
|
@@ -515,6 +517,7 @@ export function parseActions(fileName, sourceText) {
|
|
|
515
517
|
form: callee,
|
|
516
518
|
name: strProp(obj, "name"),
|
|
517
519
|
kind: strProp(obj, "kind"),
|
|
520
|
+
effect: strProp(obj, "effect"),
|
|
518
521
|
keys: objKeys(obj),
|
|
519
522
|
ident: constIdent(node),
|
|
520
523
|
exported: isExported(node),
|
|
@@ -575,6 +578,12 @@ export function lintAction(a) {
|
|
|
575
578
|
if (!a.kind) at("kind", `${a.form} sem \`kind\``);
|
|
576
579
|
else if (!KINDS.has(a.kind))
|
|
577
580
|
at("kind", `kind "${a.kind}" inválido (use simple|form|list|view)`);
|
|
581
|
+
// ADR 0041: list e view são servidas por GET — leitura pelo protocolo.
|
|
582
|
+
if ((a.kind === "list" || a.kind === "view") && a.effect === "write")
|
|
583
|
+
at(
|
|
584
|
+
"effect",
|
|
585
|
+
`${a.kind} é servida por GET e não pode declarar effect: 'write'; use simple ou form para escrever`,
|
|
586
|
+
);
|
|
578
587
|
|
|
579
588
|
// field-order: os campos conhecidos devem ter group-index não-decrescente
|
|
580
589
|
let lastGroup = -1;
|
|
@@ -585,7 +594,7 @@ export function lintAction(a) {
|
|
|
585
594
|
if (g < lastGroup) {
|
|
586
595
|
at(
|
|
587
596
|
"field-order",
|
|
588
|
-
`campo "${k}" fora de ordem (depois de "${lastKey}") — siga identidade→docs→input/output→authorize→handler→comportamento`,
|
|
597
|
+
`campo "${k}" fora de ordem (depois de "${lastKey}") — siga identidade→docs→input/output e natureza (effect, personalData)→authorize→handler→comportamento`,
|
|
589
598
|
);
|
|
590
599
|
break;
|
|
591
600
|
}
|
|
@@ -642,6 +651,11 @@ export function checkProject(sources) {
|
|
|
642
651
|
const productDeclarations = [];
|
|
643
652
|
const entityNames = new Set();
|
|
644
653
|
const actionNames = new Set();
|
|
654
|
+
// `mockHandler` obsoleto (ADR 0037): contado, não reprovado — a minor ainda o aceita.
|
|
655
|
+
let deprecatedMockHandlers = 0;
|
|
656
|
+
// Natureza da action (ADR 0041): declarações ausentes são contadas, não reprovadas, na minor.
|
|
657
|
+
let missingEffect = 0;
|
|
658
|
+
let missingPersonalData = 0;
|
|
645
659
|
|
|
646
660
|
for (const { file, text } of sources) {
|
|
647
661
|
const declarations = parseDataProducts(file, text);
|
|
@@ -666,6 +680,12 @@ export function checkProject(sources) {
|
|
|
666
680
|
if (item.name !== null) actionNames.add(item.name);
|
|
667
681
|
}
|
|
668
682
|
for (const f of lintAction(item)) findings.push({ ...f, file });
|
|
683
|
+
if (item.keys.includes("mockHandler")) deprecatedMockHandlers += 1;
|
|
684
|
+
if (item.form !== "bindAction") {
|
|
685
|
+
const writesByGet = item.kind === "list" || item.kind === "view";
|
|
686
|
+
if (!writesByGet && !item.keys.includes("effect")) missingEffect += 1;
|
|
687
|
+
if (!item.keys.includes("personalData")) missingPersonalData += 1;
|
|
688
|
+
}
|
|
669
689
|
}
|
|
670
690
|
}
|
|
671
691
|
|
|
@@ -771,6 +791,9 @@ export function checkProject(sources) {
|
|
|
771
791
|
actions,
|
|
772
792
|
contracts,
|
|
773
793
|
dataProducts: productDeclarations.length,
|
|
794
|
+
deprecatedMockHandlers,
|
|
795
|
+
missingEffect,
|
|
796
|
+
missingPersonalData,
|
|
774
797
|
findings,
|
|
775
798
|
};
|
|
776
799
|
}
|
|
@@ -1921,7 +1944,7 @@ async function walkUiFiles(dir, acc = []) {
|
|
|
1921
1944
|
return acc;
|
|
1922
1945
|
}
|
|
1923
1946
|
|
|
1924
|
-
/** Escaneia um diretório → { files, actions, contracts, findings, hasMarker }.
|
|
1947
|
+
/** Escaneia um diretório → { files, actions, contracts, deprecatedMockHandlers, findings, hasMarker }.
|
|
1925
1948
|
* `hasMarker` = o diretório é um projeto opus (tem opus.json na raiz). */
|
|
1926
1949
|
export async function scanDir(rootDir) {
|
|
1927
1950
|
rootDir = canonicalProjectDirectory(rootDir);
|
|
@@ -1951,6 +1974,9 @@ export async function scanDir(rootDir) {
|
|
|
1951
1974
|
actions: checked.actions,
|
|
1952
1975
|
contracts: checked.contracts,
|
|
1953
1976
|
dataProducts: checked.dataProducts,
|
|
1977
|
+
deprecatedMockHandlers: checked.deprecatedMockHandlers,
|
|
1978
|
+
missingEffect: checked.missingEffect,
|
|
1979
|
+
missingPersonalData: checked.missingPersonalData,
|
|
1954
1980
|
findings: [...checked.findings, ...uiFindings],
|
|
1955
1981
|
hasMarker,
|
|
1956
1982
|
};
|
package/bin/lib/gen-manifest.mjs
CHANGED
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
* successStatus?: number,
|
|
52
52
|
* public?: boolean,
|
|
53
53
|
* automatable?: boolean, // aceita execução pelo sistema (ADR 0028)
|
|
54
|
+
* effect: 'read'|'write'|null, // efeito resolvido; list/view = read (ADR 0041)
|
|
55
|
+
* personalData: boolean|null, // devolve dado pessoal; null = não declarado
|
|
54
56
|
* internal?: boolean,
|
|
55
57
|
* rateLimit?: { window, max },
|
|
56
58
|
* confirm?: ConfirmSpec,
|
|
@@ -375,6 +377,9 @@ function serializeAction(action, ctx) {
|
|
|
375
377
|
authorize: serializeAuthorize(action.authorize),
|
|
376
378
|
public: action.public === true,
|
|
377
379
|
automatable: action.automatable === true,
|
|
380
|
+
// ADR 0041: `list` e `view` são leitura pelo protocolo; null = não declarado.
|
|
381
|
+
effect: action.kind === "list" || action.kind === "view" ? "read" : (action.effect ?? null),
|
|
382
|
+
personalData: typeof action.personalData === "boolean" ? action.personalData : null,
|
|
378
383
|
internal: action.internal === true,
|
|
379
384
|
successStatus: action.successStatus,
|
|
380
385
|
input: safeToJsonSchema(action.input, ctx),
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# ADR 0037 — Dados do modo design ficam fora do contrato
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita; a primeira etapa saiu na 22.1.0, e a remoção fica para a 23.0.0.
|
|
4
|
+
- **Data:** 2026-09-28.
|
|
5
|
+
- **Relacionadas:** ADR 0002 (seeds estruturados).
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
O modo design serve para desenhar e revisar telas sem backend: `opusDesign()` monta o runtime
|
|
10
|
+
dentro do dev server, e cada action responde pelo `mockHandler` declarado no contrato. O preview de
|
|
11
|
+
sessão do Maestro depende desse modo e não tem outro propósito além de design.
|
|
12
|
+
|
|
13
|
+
Em setembro de 2026, o único consumidor, a Grand Brasil, tinha 233 `mockHandler` para 256
|
|
14
|
+
contratos, cerca de 3.200 linhas entre handlers, fixtures e estado simulado. A medição mostrou três
|
|
15
|
+
problemas:
|
|
16
|
+
|
|
17
|
+
- **O mock viaja com o contrato.** O contrato é a fatia compartilhada com a interface, então o
|
|
18
|
+
build de produção da aplicação leva os handlers simulados e suas fixtures. Um build de produção
|
|
19
|
+
da Grand Brasil continha textos das fixtures e os próprios handlers.
|
|
20
|
+
- **Metade dos mocks não acrescenta nada ao schema.** Cerca de 110 handlers devolvem um literal
|
|
21
|
+
fixo (`{ id: 'mock-x' }`) ou uma lista vazia, o que o `fake` já gera a partir do `output`.
|
|
22
|
+
- **A outra metade replica o domínio.** Cerca de 95 handlers simulam um CRUD em memória e
|
|
23
|
+
reimplementam regras do handler real, como o significado de cada filtro operacional de uma
|
|
24
|
+
listagem. Em setembro, 19 commits de funcionalidade tiveram de alterar mocks junto com o código
|
|
25
|
+
real, e nada garante que as duas versões da regra continuem iguais.
|
|
26
|
+
|
|
27
|
+
Forças em tensão:
|
|
28
|
+
|
|
29
|
+
- a aplicação real deve carregar o mínimo possível de algo que só existe para design;
|
|
30
|
+
- o preview precisa responder a qualquer action, inclusive às criadas durante a sessão;
|
|
31
|
+
- às vezes o design precisa de coerência entre respostas, como um item em cada situação ou algo
|
|
32
|
+
criado que aparece na lista em seguida;
|
|
33
|
+
- no preview, variar entre sessões é aceitável, porque dado de design não é evidência. Os seeds da
|
|
34
|
+
ADR 0002 são evidência e continuam determinísticos;
|
|
35
|
+
- o Opus é protocolo e SDK: onde os cenários ficam e se são versionados é escolha do projeto.
|
|
36
|
+
|
|
37
|
+
## Decisão
|
|
38
|
+
|
|
39
|
+
1. **O contrato deixa de declarar dados de design.** `mockHandler` sai de `ActionBase`, do
|
|
40
|
+
contrato, do `ActionDef` e do `runAction`. O runtime de produção não lê mais `OPUS_MODE` nem
|
|
41
|
+
tem ramo de design. Toda a lógica de design fica em `@softize/opus/vite`.
|
|
42
|
+
2. **Toda action responde por padrão no modo design.** O plugin registra cada contrato com um
|
|
43
|
+
executor de design que, na ausência de cenário, devolve dado gerado pelo `fake` a partir do
|
|
44
|
+
`output`:
|
|
45
|
+
- `list` devolve três páginas geradas por `fakeMany`, com `items`, `cursor` e `total`, do
|
|
46
|
+
tamanho pedido em `limit` (20 quando a entrada não pede, até 100), e respeita a paginação
|
|
47
|
+
declarada. Busca e filtros não filtram o dado gerado, porque sobre dado
|
|
48
|
+
sorteado o resultado não mostraria nada útil; quando o filtro importar para o design, o
|
|
49
|
+
caminho é um cenário;
|
|
50
|
+
- a semente é derivada do nome da action e da entrada canônica, então a mesma requisição recebe
|
|
51
|
+
a mesma resposta;
|
|
52
|
+
- quando a entrada e a saída têm `id`, a saída ecoa o `id` da entrada, desde que o schema da
|
|
53
|
+
saída aceite esse valor, para que abrir um item listado mostre o mesmo identificador;
|
|
54
|
+
- uma action ligada a um handler real (`bindAction`) mantém esse handler, como hoje.
|
|
55
|
+
3. **Cenários são registrados fora do contrato, pelo nome da action.** O plugin ganha a opção
|
|
56
|
+
`scenarios`, com um ou mais globs. Os módulos casados exportam cenários criados por
|
|
57
|
+
`defineDesignScenario(contrato, resposta)`, e a resposta pode ser um valor ou uma função tipada
|
|
58
|
+
pela entrada e pela saída do contrato. O cenário vence a resposta padrão. Um cenário para uma
|
|
59
|
+
action inexistente é erro visível no preview, não um silêncio.
|
|
60
|
+
4. **O Opus não decide onde os cenários ficam.** A opção não tem default dentro do código da
|
|
61
|
+
aplicação. O consumidor escolhe, por exemplo, uma pasta ignorada pelo git que o agente da sessão
|
|
62
|
+
preenche sob demanda. Cenários começam descartáveis: só vale discutir versionar um cenário no
|
|
63
|
+
repositório do consumidor quando o mesmo for recriado em sessões diferentes.
|
|
64
|
+
5. **Stubs de rotas externas continuam como estão.** A opção `stubs` segue atendendo superfícies
|
|
65
|
+
fora do protocolo de actions, como autenticação e motores externos.
|
|
66
|
+
|
|
67
|
+
## Consequências
|
|
68
|
+
|
|
69
|
+
- A aplicação deixa de carregar mocks. O contrato volta a descrever só a capacidade, e o build de
|
|
70
|
+
produção não inclui dados de design.
|
|
71
|
+
- Uma action nova aparece no preview sem nenhuma linha de mock.
|
|
72
|
+
- O dado padrão tem o formato certo, mas não a regra de negócio: valores de enum são sorteados, e
|
|
73
|
+
campos de uma listagem e de um detalhe só coincidem no `id`. Quando isso atrapalhar o design, o
|
|
74
|
+
caminho é um cenário.
|
|
75
|
+
- Sem cenário, não há estado entre chamadas: criar um item não o faz aparecer na lista.
|
|
76
|
+
- É uma versão major. Quem declara `mockHandler` precisa remover a declaração ou transformá-la em
|
|
77
|
+
cenário, e testes que usam `OPUS_MODE=design` com `runAction` precisam de outra forma de
|
|
78
|
+
exercitar o contrato.
|
|
79
|
+
- O template de `opus create`, a documentação "Prototipar com dados simulados", as skills
|
|
80
|
+
`create-opus-action` e `test-opus-action` e o gate de ordem de campos (`check.mjs`) mudam junto.
|
|
81
|
+
- No Maestro, que fica fora deste pacote, o preview continua igual. O agente da sessão passa a
|
|
82
|
+
escrever cenários quando precisar, orientado por uma skill própria.
|
|
83
|
+
|
|
84
|
+
## Alternativas descartadas
|
|
85
|
+
|
|
86
|
+
- **Manter o estado atual.** Preserva cenários prontos entre sessões, mas mantém o mock no bundle,
|
|
87
|
+
a regra de negócio duplicada e o custo de cada funcionalidade nova.
|
|
88
|
+
- **Remover `mockHandler` apenas no build de produção.** Resolve o bundle e nada mais. O contrato
|
|
89
|
+
continua misturando capacidade e simulação, e a duplicação de regra permanece.
|
|
90
|
+
- **Preview com backend real e dados dos seeds.** Mostra o comportamento verdadeiro e elimina os
|
|
91
|
+
mocks, mas exige banco, API e isolamento por sessão num preview que só serve a design, além de
|
|
92
|
+
rever as decisões do Maestro de manter o preview isolado e sem Postgres.
|
|
93
|
+
- **CRUD em memória genérico a partir de metadado novo no contrato.** Exigiria declarar entidade e
|
|
94
|
+
operação em cada action e, mesmo assim, não cobriria regras de negócio, que continuariam
|
|
95
|
+
escritas à mão.
|
|
96
|
+
- **Somente a resposta padrão, sem cenários.** É o mínimo absoluto, mas deixaria o design sem saída
|
|
97
|
+
quando a coerência importa. O custo do mecanismo de cenários é pequeno perto disso.
|
|
98
|
+
|
|
99
|
+
## Transição
|
|
100
|
+
|
|
101
|
+
A saída acontece em duas versões. A 22.1.0 adiciona a resposta padrão e os cenários, com
|
|
102
|
+
`mockHandler` ainda aceito e precedendo a resposta gerada, e o `check` passa a contar o campo como
|
|
103
|
+
obsoleto. O consumidor remove seus mocks domínio a domínio. A 23.0.0 remove o campo e o ramo de
|
|
104
|
+
design do runtime.
|
|
105
|
+
|
|
106
|
+
## Verificação
|
|
107
|
+
|
|
108
|
+
- testes do plugin: resposta padrão por `kind` (`view`, `list`, `form`, `simple`), páginas do
|
|
109
|
+
tamanho pedido em `limit` com a paginação declarada, mesma resposta para a mesma entrada, eco do
|
|
110
|
+
`id`, precedência do cenário e erro para cenário sem action ou duplicado;
|
|
111
|
+
- teste de tipos: `defineDesignScenario` infere entrada e saída do contrato e rejeita resposta
|
|
112
|
+
incompatível;
|
|
113
|
+
- busca estática: nenhum `OPUS_MODE` nem `mockHandler` em `src/core` e `src/testing` depois da
|
|
114
|
+
major;
|
|
115
|
+
- no consumidor: nenhum `mockHandler` nos contratos, e o build de produção sem textos das fixtures
|
|
116
|
+
antigas;
|
|
117
|
+
- typecheck, suíte do pacote, `opus check` e os gates de materialização verdes.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# ADR 0041 — Actions declaram efeito e dado pessoal
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita; saiu na 22.2.0.
|
|
4
|
+
- **Data:** 2026-09-29.
|
|
5
|
+
- **Relacionadas:** ADR 0028 (execução pelo sistema); ADR 0104 da Grand Brasil (actions são a
|
|
6
|
+
interface da IA).
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
Uma action é consumida pela interface, pela IA, por automações e por outros sistemas via MCP. Dois
|
|
11
|
+
fatos sobre ela importam a todos esses consumidores e hoje não estão declarados em lugar nenhum.
|
|
12
|
+
|
|
13
|
+
**Efeito.** Quem oferece uma action a um modelo precisa saber se ela só lê ou se altera estado. O
|
|
14
|
+
`kind` responde isso só em parte:
|
|
15
|
+
|
|
16
|
+
- `list` e `view` são servidas por `GET`, que o protocolo HTTP define como seguro, sem efeito
|
|
17
|
+
colateral;
|
|
18
|
+
- `simple` e `form` são servidas por `POST` e cobrem as duas coisas. Na Grand Brasil, em setembro de
|
|
19
|
+
2026, `sales.performance`, `doc.search`, `facts.query` e `sale.view` eram leituras declaradas como
|
|
20
|
+
`simple`.
|
|
21
|
+
|
|
22
|
+
O MCP tem uma anotação padrão para isso, `readOnlyHint`, que o Opus não preenche.
|
|
23
|
+
|
|
24
|
+
**Dado pessoal.** Uma action que devolve nome, documento, contato ou texto livre sobre pessoas expõe
|
|
25
|
+
dado pessoal a quem a executa. A aplicação precisa registrar essa exposição em toda requisição,
|
|
26
|
+
qualquer que seja o consumidor, para auditoria e LGPD. Hoje o registro da requisição (`AuditRecord`)
|
|
27
|
+
não distingue uma execução que expôs dado pessoal de outra que não expôs, e cada consumidor teria
|
|
28
|
+
de lembrar de marcar por conta própria.
|
|
29
|
+
|
|
30
|
+
Os dois fatos pertencem à action, não à IA: valem para qualquer consumidor. Por isso não cabem na
|
|
31
|
+
configuração `ai`.
|
|
32
|
+
|
|
33
|
+
## Decisão
|
|
34
|
+
|
|
35
|
+
### 1. Dois campos no contrato
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
defineContract({
|
|
39
|
+
name: 'sales.performance',
|
|
40
|
+
kind: 'simple',
|
|
41
|
+
description: 'Soma as vendas por período e dimensão.',
|
|
42
|
+
input: salesPerformanceInput,
|
|
43
|
+
output: salesPerformanceOutput,
|
|
44
|
+
effect: 'read',
|
|
45
|
+
personalData: false,
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Os dois campos ficam no grupo de `input` e `output` da ordem canônica, cobrada pelo `opus check`.
|
|
50
|
+
|
|
51
|
+
- **`effect: 'read' | 'write'`.** `list` e `view` são leitura pelo protocolo e não precisam
|
|
52
|
+
declarar; declarar `effect: 'write'` nelas é erro de configuração, recusado ao registrar a action.
|
|
53
|
+
`simple` e `form` declaram.
|
|
54
|
+
- **`personalData: boolean`.** Declara se a resposta contém dados que identificam pessoas. Não é
|
|
55
|
+
inferido de nada. `false` é uma declaração tão explícita quanto `true`; ausente significa "não
|
|
56
|
+
declarado".
|
|
57
|
+
|
|
58
|
+
### 2. O runtime marca a exposição em toda requisição
|
|
59
|
+
|
|
60
|
+
Quando uma action com `personalData: true` executa com sucesso, o `AuditRecord` passa a carregar:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
personalData: { records: number | null }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`records` conta os registros devolvidos: os itens da página de uma `list`, os elementos de um array,
|
|
67
|
+
1 para um objeto e `null` quando a forma da saída não permite contar. Numa `view`, `simple` ou `form`,
|
|
68
|
+
um campo `items` faz parte do registro, e o objeto continua contando como um.
|
|
69
|
+
|
|
70
|
+
A marca vale mesmo quando a action declara `audit: false`: nesse caso, o runtime emite um registro
|
|
71
|
+
mínimo, sem entrada nem saída e com `meta.exposureOnly`, para todos os sinks. Desligar a auditoria
|
|
72
|
+
serve para reduzir ruído, não para esconder exposição. Com `auditMode: 'strict'`, a falha de um sink
|
|
73
|
+
nesse registro mínimo derruba a requisição, como em qualquer outro registro. Com `AuditConfig.sink`, o registro vai só ao
|
|
74
|
+
sink indicado, com a marca. Não são marcados: a execução com erro, o enfileiramento em segundo plano
|
|
75
|
+
(que devolve o handle do job; a marca sai na execução do job) e a resposta simulada do modo design
|
|
76
|
+
(mock, cenário ou resposta gerada); um handler real executado no modo design continua marcado. O registro também carrega `effect`. O span da execução
|
|
77
|
+
recebe os atributos `opus.action.effect` e `opus.action.personal_data`, que são declarações
|
|
78
|
+
conhecidas antes da execução; a contagem fica no registro, porque o span é aberto antes de haver
|
|
79
|
+
saída.
|
|
80
|
+
|
|
81
|
+
A marca é do runtime, não do consumidor: tela, IA, automação e MCP passam pelo mesmo `execute`.
|
|
82
|
+
|
|
83
|
+
### 3. Projeções
|
|
84
|
+
|
|
85
|
+
- **Manifest:** cada action projeta `effect`, já resolvido para `list` e `view`, e `personalData`.
|
|
86
|
+
- **`runtime.aiTools()`:** cada ferramenta carrega `annotations.readOnlyHint` e
|
|
87
|
+
`annotations.destructiveHint`, e `metadata.personalData`. No MCP, `destructiveHint` ausente numa
|
|
88
|
+
escrita vale `true`; por isso, numa action com `effect: 'write'`, ele vai sempre explícito, a partir
|
|
89
|
+
de `ai.destructive`.
|
|
90
|
+
- **MCP:** o `ListTools` publica as mesmas `annotations` e acrescenta
|
|
91
|
+
`com.softize.opus/effect` e `com.softize.opus/personal-data` em `_meta`.
|
|
92
|
+
|
|
93
|
+
### 4. Adoção gradual
|
|
94
|
+
|
|
95
|
+
Nesta minor, os dois campos são opcionais. O `opus check` conta, numa linha de aviso que não
|
|
96
|
+
reprova, quantas actions `simple` ou `form` não declaram `effect` e quantas actions não declaram
|
|
97
|
+
`personalData`. A obrigatoriedade fica para uma major, depois que o consumidor declarar tudo.
|
|
98
|
+
|
|
99
|
+
## Consequências
|
|
100
|
+
|
|
101
|
+
- O host de IA decide pela declaração, e não por `kind`, nome ou descrição, o que entra no turno
|
|
102
|
+
sem procedimento.
|
|
103
|
+
- A auditoria de exposição de dado pessoal passa a ser propriedade do runtime.
|
|
104
|
+
- Consumidores precisam classificar retroativamente as actions que já existem. O aviso do
|
|
105
|
+
`opus check` mede o que falta.
|
|
106
|
+
- `effect` é declarativo: o runtime não impede uma action `read` de escrever. A garantia vem do
|
|
107
|
+
handler e da revisão.
|
|
108
|
+
|
|
109
|
+
## Alternativas descartadas
|
|
110
|
+
|
|
111
|
+
### Declarar na configuração `ai`
|
|
112
|
+
|
|
113
|
+
Seria menor, mas os dois fatos valem para todo consumidor. A marca de dado pessoal precisa existir
|
|
114
|
+
também em requisições da interface e de automações.
|
|
115
|
+
|
|
116
|
+
### Inferir leitura de `kind` para todos os kinds
|
|
117
|
+
|
|
118
|
+
Funciona para `list` e `view`, por causa do `GET`. Para `simple` e `form`, a inferência erraria as
|
|
119
|
+
leituras declaradas como `simple` e daria à convenção um papel de segurança.
|
|
120
|
+
|
|
121
|
+
### Classificação rica de sensibilidade
|
|
122
|
+
|
|
123
|
+
Um enum com dado financeiro, de saúde ou sigiloso seria mais expressivo, mas nenhum consumidor tem
|
|
124
|
+
política para essas categorias hoje. O campo booleano cobre a necessidade atual e pode evoluir para
|
|
125
|
+
uma classificação sem mudar o ponto de declaração.
|
|
126
|
+
|
|
127
|
+
## Verificação
|
|
128
|
+
|
|
129
|
+
- Registrar `effect: 'write'` em `list` ou `view` falha na subida do runtime.
|
|
130
|
+
- O manifest projeta `effect: 'read'` para `list` e `view` sem declaração.
|
|
131
|
+
- `aiTools()` e o `ListTools` do MCP publicam `readOnlyHint` a partir do efeito resolvido.
|
|
132
|
+
- Execução bem-sucedida de action com `personalData: true` produz `AuditRecord.personalData` com a
|
|
133
|
+
contagem, e o span recebe os atributos de efeito e de dado pessoal.
|
|
134
|
+
- Execução com erro, enfileiramento em segundo plano, modo design e action com `personalData: false`
|
|
135
|
+
não produzem a marca; a execução do job produz.
|
|
136
|
+
- Com `audit: false`, a exposição gera registro mínimo, sem entrada nem saída.
|
|
137
|
+
- `records` conta a página numa `list` e 1 numa `view` com campo `items`.
|
|
138
|
+
- Escrita sem `ai.destructive` publica `destructiveHint: false`.
|
|
139
|
+
- O `opus check` conta as declarações ausentes sem reprovar.
|
package/docs/protocol.md
CHANGED
|
@@ -79,6 +79,10 @@ type ActionBase<In, Out> = {
|
|
|
79
79
|
input: Schema<In> // StandardSchemaV1-compatible
|
|
80
80
|
output: Schema<Out>
|
|
81
81
|
|
|
82
|
+
// — Natureza (ADR 0041) —
|
|
83
|
+
effect?: 'read' | 'write' // list/view = read pelo GET; simple/form declaram
|
|
84
|
+
personalData?: boolean // devolve dado que identifica pessoas; marca o audit
|
|
85
|
+
|
|
82
86
|
// — Autorização (fail-closed default) —
|
|
83
87
|
public?: boolean // default false
|
|
84
88
|
requires?: string | string[] // permissão declarativa (drive OpenAPI/docs)
|
|
@@ -628,6 +632,8 @@ Toda action **audita por default**. Para desabilitar (raramente justificável),
|
|
|
628
632
|
|
|
629
633
|
> Audit é o **log de negócio** de mudanças de estado. Não confundir com logging de aplicação (debug, info) — esses ficam por conta do logger do projeto.
|
|
630
634
|
|
|
635
|
+
**Exposição de dado pessoal (ADR 0041).** Quando a action declara `personalData: true` e executa com sucesso, o registro carrega `personalData: { records }`, com a quantidade de registros devolvidos (os itens da página de uma `list`, 1 para um objeto, `null` quando a saída não permite contar). A marca é do runtime: vale para a interface, a IA, automações e o MCP, sem que o consumidor precise lembrar. O registro também carrega `effect`. Não recebem a marca: a execução com erro; o enfileiramento de uma action em segundo plano, que devolve o handle do job (a marca sai na execução do job); e a resposta simulada do modo design (mock, cenário ou resposta gerada); um handler real executado no modo design continua marcado. Com `audit: false`, a execução que expôs dado pessoal ainda gera um registro mínimo, sem entrada nem saída e com `meta.exposureOnly`, entregue a todos os sinks. Com `auditMode: 'strict'`, a falha de um sink nesse registro derruba a requisição, como em qualquer outro registro. Com `AuditConfig.sink`, o registro, com a marca, vai só ao sink indicado. `records` conta os itens da página numa `list`; em `view`, `simple` e `form`, um objeto conta como um registro, mesmo que tenha um campo `items`.
|
|
636
|
+
|
|
631
637
|
### Shape do registro
|
|
632
638
|
|
|
633
639
|
```ts
|
|
@@ -649,6 +655,11 @@ type AuditRecord = {
|
|
|
649
655
|
output?: unknown; // já redacted, presente apenas em success
|
|
650
656
|
error?: ActionError; // presente apenas em outcome=error
|
|
651
657
|
|
|
658
|
+
effect?: 'read' | 'write'; // efeito resolvido (ADR 0041); list/view = read
|
|
659
|
+
personalData?: { // exposição de dado pessoal (ADR 0041)
|
|
660
|
+
records: number | null; // registros devolvidos; null = não contável
|
|
661
|
+
};
|
|
662
|
+
|
|
652
663
|
severity: 'info' | 'warning' | 'error';
|
|
653
664
|
trace?: { // campo legado — compatibilidade
|
|
654
665
|
requestId?: string;
|
|
@@ -667,8 +678,8 @@ defineAction({
|
|
|
667
678
|
})
|
|
668
679
|
|
|
669
680
|
defineAction({
|
|
670
|
-
audit: false, // desabilita (precisa justificar em PR)
|
|
671
|
-
})
|
|
681
|
+
audit: false, // desabilita (precisa justificar em PR); exposição de
|
|
682
|
+
}) // dado pessoal ainda gera registro mínimo (ADR 0041)
|
|
672
683
|
|
|
673
684
|
defineAction({
|
|
674
685
|
audit: {
|
|
@@ -2171,6 +2182,8 @@ chamada, inclusive por IA e MCP.
|
|
|
2171
2182
|
- **`automatable`** — opt-in que permite a um schedule executar a action como o sistema, sem usuário. Negado por padrão; não abre a action a anônimos. Distinta de `ai.enabled`. Ver §4.
|
|
2172
2183
|
- **`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.
|
|
2173
2184
|
- **`internal`** — flag que indica action service-to-service apenas; server adapter não expõe como REST público. Ver §2.
|
|
2185
|
+
- **`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.
|
|
2186
|
+
- **`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.
|
|
2174
2187
|
- **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.
|
|
2175
2188
|
- **`defineReaction`** — função que constrói uma reaction declarativa.
|
|
2176
2189
|
- **`ReactionContext`** — contexto entregue ao handler de reaction (user, tenant, db, emit, log, meta).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softize/opus",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.2.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",
|
|
@@ -173,6 +173,10 @@
|
|
|
173
173
|
"types": "./src/seed/index.ts",
|
|
174
174
|
"default": "./src/seed/index.ts"
|
|
175
175
|
},
|
|
176
|
+
"./design": {
|
|
177
|
+
"types": "./src/design/index.ts",
|
|
178
|
+
"default": "./src/design/index.ts"
|
|
179
|
+
},
|
|
176
180
|
"./presentation": {
|
|
177
181
|
"types": "./src/presentation/index.ts",
|
|
178
182
|
"default": "./src/presentation/index.ts"
|
|
@@ -15,6 +15,7 @@ registrados no runtime do projeto.
|
|
|
15
15
|
|
|
16
16
|
- nome `<resource>.<verb>` e `kind` (`simple`, `form`, `list` ou `view`);
|
|
17
17
|
- comportamento, autorização, input, output, erros e efeitos esperados;
|
|
18
|
+
- se a action só lê ou altera estado e se a resposta contém dados que identificam pessoas;
|
|
18
19
|
- caminhos reais de `shared` e `api` adotados pelo repo.
|
|
19
20
|
|
|
20
21
|
## Procedimento
|
|
@@ -23,8 +24,12 @@ registrados no runtime do projeto.
|
|
|
23
24
|
2. Colocar o contrato em módulo importável pelos consumidores e o binding no lado servidor.
|
|
24
25
|
3. Substituir os exemplos do scaffold por schemas, descrições e comportamento do domínio.
|
|
25
26
|
4. Declarar `requires` somente com `authorize` efetivo; `requires` sozinho não protege a action.
|
|
26
|
-
5.
|
|
27
|
-
|
|
27
|
+
5. Declarar `effect` em `simple` e `form` e `personalData` em todos os kinds. O scaffold parte de
|
|
28
|
+
`effect: 'write'` e deixa `personalData` comentado de propósito: a ausência aparece no aviso do
|
|
29
|
+
`opus check` até alguém decidir, porque um `false` preenchido por padrão esconderia exposição de
|
|
30
|
+
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.
|
|
28
33
|
|
|
29
34
|
## Verificação
|
|
30
35
|
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
# Contrato e binding
|
|
2
2
|
|
|
3
|
-
O contrato contém `name`, `kind`, documentação, schemas, metadata de UI
|
|
4
|
-
declarativa compartilhável
|
|
3
|
+
O contrato contém `name`, `kind`, documentação, schemas, metadata de UI e autorização
|
|
4
|
+
declarativa compartilhável. Ele não declara dados de design: no modo design, a action sem mock já
|
|
5
|
+
responde com dado gerado a partir do `output`, e um cenário específico, quando necessário, fica
|
|
6
|
+
fora do contrato em `defineDesignScenario` (ADR 0037). `mockHandler` está obsoleto.
|
|
5
7
|
|
|
6
8
|
O binding contém `handler`, `loads`, autorização dependente de dados carregados, execução
|
|
7
9
|
em background, eventos e idempotência. `bindAction(contract, binding)` produz o `ActionDef`
|
|
8
10
|
registrado no runtime.
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
O contrato também declara o que a action faz com os dados (ADR 0041): `effect` (`read` ou
|
|
13
|
+
`write`) em `simple` e `form`, porque `list` e `view` já são leitura pelo `GET`, e `personalData`
|
|
14
|
+
em todos os kinds. Os dois fatos alimentam a auditoria de exposição de dado pessoal, as ferramentas
|
|
15
|
+
de IA e as anotações do MCP.
|
|
16
|
+
|
|
17
|
+
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
|
|
12
19
|
servem e o scaffold acompanha a forma atual do SDK.
|
|
@@ -3,3 +3,6 @@
|
|
|
3
3
|
- Dispara: “Crie a action `invoice.approve` no Opus.”
|
|
4
4
|
- Não dispara: “Crie uma função TypeScript que soma duas parcelas.”
|
|
5
5
|
- Execução: gerar contrato e binding para cada kind e obter zero findings no `opus check`.
|
|
6
|
+
- Execução: “Crie `customer.list`, que lista clientes com nome e telefone” resulta em
|
|
7
|
+
`personalData: true` declarado; “Crie `sales.performance`, que soma vendas por mês” resulta em
|
|
8
|
+
`effect: 'read'` e `personalData: false`.
|
|
@@ -17,8 +17,8 @@ ${common}
|
|
|
17
17
|
description: 'Descreva o comportamento de negócio e seus limites.',
|
|
18
18
|
input: z.object({}),
|
|
19
19
|
output: z.object({ id: z.string() }),
|
|
20
|
+
// personalData: decida — true se a resposta identifica pessoas (nome, documento, contato).
|
|
20
21
|
paginate: 'cursor',
|
|
21
|
-
mockHandler: async () => ({ items: [], cursor: { next: null }, total: 0 }),
|
|
22
22
|
})`
|
|
23
23
|
}
|
|
24
24
|
if (kind === 'form') {
|
|
@@ -29,8 +29,9 @@ ${common}
|
|
|
29
29
|
messages: { success: 'Operação concluída.', error: 'Não foi possível concluir.' },
|
|
30
30
|
input: z.object({ id: z.string() }),
|
|
31
31
|
output: z.object({ id: z.string() }),
|
|
32
|
+
effect: 'write',
|
|
33
|
+
// personalData: decida — true se a resposta identifica pessoas (nome, documento, contato).
|
|
32
34
|
fields: { id: { label: 'Identificador' } },
|
|
33
|
-
mockHandler: async (_ctx, input) => ({ id: input.id }),
|
|
34
35
|
})`
|
|
35
36
|
}
|
|
36
37
|
if (kind === 'view') {
|
|
@@ -40,7 +41,7 @@ ${common}
|
|
|
40
41
|
description: 'Descreva o comportamento de negócio e seus limites.',
|
|
41
42
|
input: z.object({ id: z.string() }),
|
|
42
43
|
output: z.object({ id: z.string() }),
|
|
43
|
-
|
|
44
|
+
// personalData: decida — true se a resposta identifica pessoas (nome, documento, contato).
|
|
44
45
|
})`
|
|
45
46
|
}
|
|
46
47
|
return `export const ${identifier} = defineContract({
|
|
@@ -50,7 +51,8 @@ ${common}
|
|
|
50
51
|
messages: { success: 'Operação concluída.', error: 'Não foi possível concluir.' },
|
|
51
52
|
input: z.object({ id: z.string() }),
|
|
52
53
|
output: z.object({ ok: z.boolean() }),
|
|
53
|
-
|
|
54
|
+
effect: 'write',
|
|
55
|
+
// personalData: decida — true se a resposta identifica pessoas (nome, documento, contato).
|
|
54
56
|
})`
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -12,8 +12,8 @@ infraestrutura real quando a unidade é o contrato.
|
|
|
12
12
|
|
|
13
13
|
## Procedimento
|
|
14
14
|
|
|
15
|
-
1. Importar o `ActionDef` produzido pelo binding
|
|
16
|
-
|
|
15
|
+
1. Importar o `ActionDef` produzido pelo binding. Dados do modo design não são comportamento da
|
|
16
|
+
action e não entram nestes testes.
|
|
17
17
|
2. Executar com `runAction` e fornecer `user`, `can`, `loaded`, storage ou outros adapters
|
|
18
18
|
pelas opções do harness.
|
|
19
19
|
3. Cobrir sucesso, input inválido, autorização negada e erros de negócio materiais.
|