@softize/opus 15.2.1 → 15.2.2

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 CHANGED
@@ -7,6 +7,16 @@ 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
+ ## 15.2.2 — 2026-09-11
11
+
12
+ O acesso descritivo de Produtos de Dados usa `permissionContexts` no lugar do nome genérico
13
+ `contexts`. A qualificação evita confundir contextos do framework, contexto de tela, domínio de
14
+ dados e vocabulário de autorização nas projeções do manifest e da Lens.
15
+
16
+ **Migração recomendada:** em declarações `defineDataProduct`, renomeie `access.contexts` para
17
+ `access.permissionContexts` e regenere o manifest. O nome anterior continua aceito e projetado em
18
+ toda a série 15.x como alias deprecated; consumidores novos devem ler somente o nome qualificado.
19
+
10
20
  ## 15.2.1 — 2026-09-11
11
21
 
12
22
  `opus copy` agora poda diretórios cobertos por um glob recursivo de `copy.exclude`, em vez de
@@ -220,7 +220,16 @@ function serializeDataProducts(source) {
220
220
  : [],
221
221
  entities: Array.isArray(product.entities) ? product.entities : [],
222
222
  access: {
223
- contexts: Array.isArray(product.access?.contexts) ? product.access.contexts : [],
223
+ permissionContexts: Array.isArray(product.access?.permissionContexts)
224
+ ? product.access.permissionContexts
225
+ : Array.isArray(product.access?.contexts)
226
+ ? product.access.contexts
227
+ : [],
228
+ contexts: Array.isArray(product.access?.permissionContexts)
229
+ ? product.access.permissionContexts
230
+ : Array.isArray(product.access?.contexts)
231
+ ? product.access.contexts
232
+ : [],
224
233
  organizationalScopes: Array.isArray(product.access?.organizationalScopes)
225
234
  ? product.access.organizationalScopes
226
235
  : [],
@@ -27,10 +27,12 @@ descritivo e Actions de interface. Produtos ativos podem ser descontinuados com
27
27
  duplicados e relações literais que apontam para Action ou Entity inexistente. A declaração não
28
28
  executa consulta, não contém driver e não substitui uma Action.
29
29
 
30
- A autorização continua sendo responsabilidade da Action. `access.contexts` e
30
+ A autorização continua sendo responsabilidade da Action. `access.permissionContexts` e
31
31
  `access.organizationalScopes` documentam o alcance esperado para catálogo, Lens e revisão, mas o
32
32
  runtime não os converte em autorização implícita. Essa separação impede que uma descrição
33
- incompleta abra dados.
33
+ incompleta abra dados. O alias histórico `access.contexts` permanece aceito como entrada e
34
+ projetado no manifest durante a série 15.x por compatibilidade. Novos consumidores usam o nome
35
+ qualificado; o alias será removido somente numa versão major.
34
36
 
35
37
  O manifest projeta a declaração integral. Tools de IA recebem a lista de produtos que expõem em
36
38
  metadata, e o servidor MCP publica essa lista em `_meta['com.softize.opus/data-products']`. A
@@ -24,7 +24,7 @@ export const salesLeads = defineDataProduct({
24
24
  sources: [{ id: 'followize', label: 'Followize' }],
25
25
  entities: ['Lead'],
26
26
  access: {
27
- contexts: ['sales'],
27
+ permissionContexts: ['sales'],
28
28
  organizationalScopes: ['unit', 'team'],
29
29
  },
30
30
  interfaces: ['sale.list', 'sales.performance'],
@@ -49,6 +49,12 @@ que uma interface deveria respeitar, por exemplo, unidade e equipe. Não é RLS
49
49
  automática. Toda interface precisa aplicar seus próprios `requires`, `authorize` e recortes no
50
50
  handler/repositório, inclusive quando for chamada por IA ou MCP.
51
51
 
52
+ `permissionContexts` nomeia especificamente chaves do vocabulário de permissão. Ele não descreve
53
+ domínio de dados, Área, departamento nem contexto de tela; projetos que não adotam essa dimensão
54
+ declaram uma lista vazia. O alias `contexts`, publicado originalmente na série 15.x, permanece
55
+ aceito na entrada e projetado no manifest apenas para compatibilidade. Novas declarações e
56
+ consumidores usam o nome qualificado; o alias só poderá ser removido numa versão major.
57
+
52
58
  ## Projeções
53
59
 
54
60
  `opus gen` publica os produtos no `.opus/manifest.json`. Actions expostas como tools carregam os
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "15.2.1",
3
+ "version": "15.2.2",
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",
@@ -212,17 +212,6 @@
212
212
  "bin": {
213
213
  "opus": "bin/cli.mjs"
214
214
  },
215
- "scripts": {
216
- "postinstall": "node ./bin/lib/postinstall.mjs",
217
- "copy:check": "node ./bin/cli.mjs copy --check",
218
- "typecheck": "tsc --noEmit",
219
- "test": "vitest run",
220
- "test:watch": "vitest",
221
- "test:cov": "vitest run --coverage",
222
- "registry:up": "npx -y verdaccio --config ~/.config/verdaccio/config.yaml",
223
- "release": "bash ./scripts/release.sh",
224
- "release:local": "bash ./scripts/release.sh --local"
225
- },
226
215
  "dependencies": {
227
216
  "@modelcontextprotocol/sdk": "^1.29.0",
228
217
  "@radix-ui/react-checkbox": "^1.1.3",
@@ -368,11 +357,21 @@
368
357
  "vitest": "^2.1.0",
369
358
  "zod": "^3.24.0"
370
359
  },
371
- "packageManager": "pnpm@9.0.0",
372
360
  "repository": {
373
361
  "type": "git",
374
362
  "url": "git+https://github.com/softize-dev/opus.git",
375
363
  "directory": "packages/opus"
376
364
  },
377
- "homepage": "https://opus.softize.com.br"
378
- }
365
+ "homepage": "https://opus.softize.com.br",
366
+ "scripts": {
367
+ "postinstall": "node ./bin/lib/postinstall.mjs",
368
+ "copy:check": "node ./bin/cli.mjs copy --check",
369
+ "typecheck": "tsc --noEmit",
370
+ "test": "vitest run",
371
+ "test:watch": "vitest",
372
+ "test:cov": "vitest run --coverage",
373
+ "registry:up": "npx -y verdaccio --config ~/.config/verdaccio/config.yaml",
374
+ "release": "bash ./scripts/release.sh",
375
+ "release:local": "bash ./scripts/release.sh --local"
376
+ }
377
+ }
@@ -14,12 +14,21 @@ export interface DataProductSource {
14
14
  }
15
15
 
16
16
  export interface DataProductAccess {
17
- /** Contextos de dados normalmente exigidos pelas interfaces do produto. Descritivo. */
17
+ /** @deprecated Use `permissionContexts`; este alias será removido numa versão major. */
18
18
  contexts: readonly string[]
19
+ /** Nome qualificado dos contextos de permissão. Sempre presente após `defineDataProduct`. */
20
+ permissionContexts?: readonly string[]
19
21
  /** Eixos organizacionais que as Actions precisam considerar, como unit e team. Descritivo. */
20
22
  organizationalScopes: readonly string[]
21
23
  }
22
24
 
25
+ export interface DataProductAccessInput {
26
+ permissionContexts?: readonly string[]
27
+ /** @deprecated Use `permissionContexts`; este alias será removido numa versão major. */
28
+ contexts?: readonly string[]
29
+ organizationalScopes: readonly string[]
30
+ }
31
+
23
32
  export interface DataProductConfig {
24
33
  /** Identidade estável e namespaced, como `sales.leads`. */
25
34
  id: string
@@ -46,6 +55,19 @@ export interface DataProductConfig {
46
55
  replacedBy?: string
47
56
  }
48
57
 
58
+ export type DataProductInput = Omit<DataProductConfig, 'access'> & { access: DataProductAccessInput }
59
+ type PermissionContextsOf<T extends DataProductAccessInput> = Extract<
60
+ | ('permissionContexts' extends keyof T ? T['permissionContexts'] : never)
61
+ | ('contexts' extends keyof T ? T['contexts'] : never),
62
+ readonly string[]
63
+ >
64
+ export type DefinedDataProduct<T extends DataProductInput = DataProductInput> = Omit<T, 'access'> & {
65
+ access: Omit<T['access'], 'contexts' | 'permissionContexts'> & DataProductAccess & {
66
+ contexts: PermissionContextsOf<T['access']>
67
+ permissionContexts: PermissionContextsOf<T['access']>
68
+ }
69
+ }
70
+
49
71
  const ID_RE = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$/
50
72
 
51
73
  function nonEmpty(value: unknown, field: string): void {
@@ -55,7 +77,7 @@ function nonEmpty(value: unknown, field: string): void {
55
77
  }
56
78
 
57
79
  /** Declara e valida um Produto de Dados sem introduzir dependência de banco ou runtime. */
58
- export function defineDataProduct<const T extends DataProductConfig>(config: T): T {
80
+ export function defineDataProduct<const T extends DataProductInput>(config: T): DefinedDataProduct<T> {
59
81
  if (!ID_RE.test(config.id)) {
60
82
  throw new TypeError(`DataProduct id "${config.id}" deve ser namespaced e casar com ${ID_RE.source}`)
61
83
  }
@@ -90,10 +112,23 @@ export function defineDataProduct<const T extends DataProductConfig>(config: T):
90
112
  throw new TypeError(`DataProduct "${config.id}" possui interface duplicada`)
91
113
  }
92
114
  for (const action of config.interfaces) nonEmpty(action, 'interfaces[]')
93
- if (!Array.isArray(config.access?.contexts) || !Array.isArray(config.access?.organizationalScopes)) {
94
- throw new TypeError('DataProduct "access" deve declarar contexts e organizationalScopes')
115
+ const hasPermissionContexts = Array.isArray(config.access?.permissionContexts)
116
+ const hasLegacyContexts = Array.isArray(config.access?.contexts)
117
+ if ((!hasPermissionContexts && !hasLegacyContexts) || !Array.isArray(config.access?.organizationalScopes)) {
118
+ throw new TypeError('DataProduct "access" deve declarar permissionContexts e organizationalScopes')
119
+ }
120
+ if (
121
+ hasPermissionContexts &&
122
+ hasLegacyContexts &&
123
+ (config.access.permissionContexts!.length !== config.access.contexts!.length ||
124
+ config.access.permissionContexts!.some((context, index) => context !== config.access.contexts![index]))
125
+ ) {
126
+ throw new TypeError('DataProduct "access" recebeu permissionContexts e contexts divergentes')
95
127
  }
96
- for (const context of config.access.contexts) nonEmpty(context, 'access.contexts[]')
128
+ const permissionContexts = (
129
+ hasPermissionContexts ? config.access.permissionContexts : config.access.contexts
130
+ ) as PermissionContextsOf<T['access']>
131
+ for (const context of permissionContexts) nonEmpty(context, 'access.permissionContexts[]')
97
132
  for (const scope of config.access.organizationalScopes) nonEmpty(scope, 'access.organizationalScopes[]')
98
133
  if (config.status !== undefined && config.status !== 'active' && config.status !== 'deprecated') {
99
134
  throw new TypeError('DataProduct "status" deve ser "active" ou "deprecated"')
@@ -107,13 +142,22 @@ export function defineDataProduct<const T extends DataProductConfig>(config: T):
107
142
  } else if (config.replacedBy !== undefined) {
108
143
  throw new TypeError('DataProduct ativo não pode declarar "replacedBy"')
109
144
  }
110
- return config
145
+ return {
146
+ ...config,
147
+ access: {
148
+ ...config.access,
149
+ permissionContexts,
150
+ contexts: permissionContexts,
151
+ },
152
+ } as unknown as DefinedDataProduct<T>
111
153
  }
112
154
 
113
155
  export function isDataProduct(value: unknown): value is DataProductConfig {
114
156
  if (typeof value !== 'object' || value === null) return false
157
+ const access = (value as { access?: { contexts?: unknown } }).access
158
+ if (!Array.isArray(access?.contexts)) return false
115
159
  try {
116
- defineDataProduct(value as DataProductConfig)
160
+ defineDataProduct(value as DataProductInput)
117
161
  return true
118
162
  } catch {
119
163
  return false
package/src/core/index.ts CHANGED
@@ -204,7 +204,10 @@ export type { DomainConfig, FlattenedDomain } from './domain.ts'
204
204
  export { defineDataProduct, isDataProduct } from './data-product.ts'
205
205
  export type {
206
206
  DataProductAccess,
207
+ DataProductAccessInput,
207
208
  DataProductConfig,
209
+ DataProductInput,
210
+ DefinedDataProduct,
208
211
  DataProductSource,
209
212
  DataProductStatus,
210
213
  } from './data-product.ts'