@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/cli.mjs +18 -1
  3. package/bin/lib/check.mjs +28 -2
  4. package/bin/lib/gen-manifest.mjs +2 -0
  5. package/bin/lib/gen-runner.mjs +5 -0
  6. package/docs/adr/0037-design-data-lives-outside-the-contract.md +117 -0
  7. package/docs/adr/0041-actions-declare-effect-and-personal-data.md +139 -0
  8. package/docs/protocol.md +15 -2
  9. package/package.json +5 -1
  10. package/registry/skills/create-opus-action/SKILL.md +7 -2
  11. package/registry/skills/create-opus-action/references/contract-and-binding.md +11 -4
  12. package/registry/skills/create-opus-action/references/evaluations.md +3 -0
  13. package/registry/skills/create-opus-action/scripts/scaffold.mjs +6 -4
  14. package/registry/skills/test-opus-action/SKILL.md +2 -2
  15. package/registry/skills/test-opus-action/references/harness.md +2 -2
  16. package/registry/templates/app/src/App.tsx +1 -1
  17. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +2 -19
  18. package/registry/templates/app/src/domains/tasks/actions/list.ts +5 -5
  19. package/registry/templates/app/vite.config.ts +3 -2
  20. package/src/core/action-nature.ts +40 -0
  21. package/src/core/audit.ts +12 -4
  22. package/src/core/domain.ts +2 -2
  23. package/src/core/index.ts +2 -0
  24. package/src/core/runtime.ts +56 -1
  25. package/src/core/types.ts +46 -20
  26. package/src/design/index.ts +179 -0
  27. package/src/mcp/index.ts +6 -0
  28. package/src/testing/fake.ts +2 -2
  29. package/src/testing/index.ts +7 -3
  30. package/src/ui/docs/content/actions.md +79 -44
  31. package/src/ui/docs/content/mcp.md +6 -0
  32. package/src/ui/docs/content/testing.md +9 -25
  33. 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 } = await scanDir(root)
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
  };
@@ -53,6 +53,8 @@ function shapeAction(a) {
53
53
  authorize: a.authorize,
54
54
  public: a.public,
55
55
  automatable: a.automatable,
56
+ effect: a.effect ?? null,
57
+ personalData: a.personalData ?? null,
56
58
  internal: a.internal,
57
59
  successStatus: a.successStatus,
58
60
  input: a.input,
@@ -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.0.0",
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. Exportar e registrar o binding no domínio/runtime existente.
27
- 6. Carregar e seguir `$test-opus-action` para cobrir contrato, sucesso, erros e efeitos relevantes.
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, autorização
4
- declarativa compartilhável e `mockHandler` de design quando aplicá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
- Ordem cobrada pelo check: identidade → documentação → input/output → autorização →
11
- handler/mock → comportamento. Campos específicos do kind permanecem junto do grupo ao qual
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
- mockHandler: async (_ctx, input) => ({ id: input.id }),
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
- mockHandler: async () => ({ ok: true }),
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; para contrato puro, testar `mockHandler`
16
- apenas no modo design.
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.