@softize/opus 15.2.0 → 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,25 @@ 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
+
20
+ ## 15.2.1 — 2026-09-11
21
+
22
+ `opus copy` agora poda diretórios cobertos por um glob recursivo de `copy.exclude`, em vez de
23
+ percorrer toda a árvore para só então descartar seus arquivos. Isso evita custo desnecessário e
24
+ travamentos aparentes em repositórios que mantêm worktrees ou artefatos volumosos dentro da raiz.
25
+
26
+ **Migração:** nenhuma. Projetos com diretórios locais volumosos devem declará-los em
27
+ `copy.exclude` com um glob recursivo, como `.worktrees/**`.
28
+
10
29
  ## 15.2.0 — 2026-09-11
11
30
 
12
31
  Produtos de Dados passam a ser declarações de primeira classe com `defineDataProduct` e registro
package/bin/lib/copy.mjs CHANGED
@@ -2146,6 +2146,14 @@ function globRegex(pattern) {
2146
2146
  function sourceFiles(root, excluded = []) {
2147
2147
  const output = []
2148
2148
  const diagnostics = []
2149
+ const excludedPatterns = excluded.map((pattern) => ({
2150
+ regex: globRegex(pattern),
2151
+ subtree: pattern.endsWith('/**') ? globRegex(pattern.slice(0, -3)) : null,
2152
+ }))
2153
+ const isExcluded = (source) => excludedPatterns.some(({ regex }) => regex.test(source))
2154
+ const isExcludedDirectory = (source) => excludedPatterns.some(({ regex, subtree }) =>
2155
+ regex.test(source) || subtree?.test(source),
2156
+ )
2149
2157
  const walk = (directory = '.') => {
2150
2158
  let listing
2151
2159
  try {
@@ -2162,8 +2170,9 @@ function sourceFiles(root, excluded = []) {
2162
2170
  const target = path.join(directory, item.name)
2163
2171
  const source = portable(target === '.' ? item.name : target)
2164
2172
  if (IGNORED_DIRECTORIES.has(item.name)) continue
2173
+ if (item.isDirectory() && isExcludedDirectory(source)) continue
2165
2174
  if (item.isSymbolicLink()) {
2166
- if (!excluded.some((pattern) => globRegex(pattern).test(source))) {
2175
+ if (!isExcluded(source) && !isExcludedDirectory(source)) {
2167
2176
  diagnostics.push({
2168
2177
  source,
2169
2178
  line: 1,
@@ -2189,7 +2198,7 @@ function sourceFiles(root, excluded = []) {
2189
2198
  item.isFile() &&
2190
2199
  SOURCE_EXTENSIONS.has(path.extname(item.name)) &&
2191
2200
  !IGNORED_FILE.test(item.name) &&
2192
- !excluded.some((pattern) => globRegex(pattern).test(source))
2201
+ !isExcluded(source)
2193
2202
  ) {
2194
2203
  try {
2195
2204
  const inspected = safeProjectPath(root, target, { mustExist: true })
@@ -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.0",
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'