@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 +19 -0
- package/bin/lib/copy.mjs +11 -2
- package/bin/lib/gen-runner.mjs +10 -1
- package/docs/adr/0012-data-products-are-first-class-declarations.md +4 -2
- package/docs/data-products.md +7 -1
- package/package.json +14 -15
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +0 -0
- package/src/core/data-product.ts +51 -7
- package/src/core/index.ts +3 -0
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 (!
|
|
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
|
-
!
|
|
2201
|
+
!isExcluded(source)
|
|
2193
2202
|
) {
|
|
2194
2203
|
try {
|
|
2195
2204
|
const inspected = safeProjectPath(root, target, { mustExist: true })
|
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -220,7 +220,16 @@ function serializeDataProducts(source) {
|
|
|
220
220
|
: [],
|
|
221
221
|
entities: Array.isArray(product.entities) ? product.entities : [],
|
|
222
222
|
access: {
|
|
223
|
-
|
|
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.
|
|
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
|
package/docs/data-products.md
CHANGED
|
@@ -24,7 +24,7 @@ export const salesLeads = defineDataProduct({
|
|
|
24
24
|
sources: [{ id: 'followize', label: 'Followize' }],
|
|
25
25
|
entities: ['Lead'],
|
|
26
26
|
access: {
|
|
27
|
-
|
|
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.
|
|
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
|
+
}
|
|
File without changes
|
package/src/core/data-product.ts
CHANGED
|
@@ -14,12 +14,21 @@ export interface DataProductSource {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface DataProductAccess {
|
|
17
|
-
/**
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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'
|