@softize/opus 13.0.0 → 14.0.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 +81 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/copy.mjs +276 -6
- package/bin/lib/db.mjs +16 -74
- package/bin/lib/gen-openapi.mjs +3 -3
- package/bin/lib/gen-runner.mjs +1 -1
- package/bin/lib/gen.mjs +14 -69
- package/bin/lib/mcp.mjs +3 -1
- package/bin/lib/seed.mjs +5 -62
- package/docs/code-style.md +4 -1
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/instructions/opus.md +3 -3
- package/registry/templates/app/package.json +1 -1
- package/registry/templates/app/src/App.tsx +11 -6
- package/registry/templates/app/src/main.tsx +4 -4
- package/src/audit/drivers/console.ts +1 -0
- package/src/auth/drivers/better-auth.ts +1 -0
- package/src/auth/drivers/jwt.ts +1 -0
- package/src/cache/drivers/memory.ts +1 -0
- package/src/client/drivers/fetch.ts +2 -1
- package/src/core/actions.ts +6 -1
- package/src/core/audit.ts +9 -3
- package/src/core/contracts.ts +7 -0
- package/src/core/domain.ts +1 -1
- package/src/core/errors.ts +18 -15
- package/src/core/index.ts +4 -2
- package/src/core/package-version.ts +26 -0
- package/src/core/reactions.ts +1 -1
- package/src/core/runtime.ts +33 -23
- package/src/core/schedules.ts +1 -1
- package/src/core/types.ts +5 -6
- package/src/dsl/eval.ts +2 -2
- package/src/dsl/kysely.ts +2 -2
- package/src/dsl/loads.ts +1 -1
- package/src/dsl/parser.ts +5 -5
- package/src/events/drivers/mitt.ts +1 -0
- package/src/mcp/index.ts +2 -1
- package/src/observability/drivers/opentelemetry.ts +1 -0
- package/src/queue/drivers/bullmq.ts +3 -3
- package/src/scheduler/drivers/node-cron.ts +3 -2
- package/src/scheduler/every.ts +7 -7
- package/src/schema/openapi.ts +3 -3
- package/src/seed/index.ts +29 -0
- package/src/server/drivers/fastify.ts +5 -2
- package/src/server/drivers/node.ts +9 -6
- package/src/server/index.ts +3 -1
- package/src/storage/drivers/fs.ts +1 -0
- package/src/testing/index.ts +3 -3
- package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
- package/src/ui/components/patterns/confirm.tsx +2 -31
- package/src/ui/components/patterns/content-header.tsx +44 -137
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +26 -22
- package/src/ui/components/patterns/list.tsx +18 -12
- package/src/ui/components/patterns/page-state.tsx +39 -51
- package/src/ui/components/patterns/page.tsx +37 -54
- package/src/ui/components/patterns/sidebar.tsx +17 -6
- package/src/ui/components/patterns/state-surface.tsx +148 -0
- package/src/ui/components/patterns/surface-header.tsx +119 -0
- package/src/ui/components/patterns/trigger.tsx +21 -25
- package/src/ui/components/patterns/view.tsx +29 -22
- package/src/ui/components/primitives/alert.tsx +1 -27
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +5 -41
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +39 -30
- package/src/ui/components/primitives/calendar.tsx +28 -2
- package/src/ui/components/primitives/carousel.tsx +3 -3
- package/src/ui/components/primitives/chat.tsx +1 -1
- package/src/ui/components/primitives/checkbox.tsx +1 -1
- package/src/ui/components/primitives/command.tsx +2 -2
- package/src/ui/components/primitives/control.ts +72 -0
- package/src/ui/components/primitives/copyable.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +12 -7
- package/src/ui/components/primitives/dot.tsx +1 -25
- package/src/ui/components/primitives/drawer.tsx +10 -3
- package/src/ui/components/primitives/field.tsx +3 -3
- package/src/ui/components/primitives/icon-picker.tsx +3 -1
- package/src/ui/components/primitives/input-group.tsx +12 -9
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +2 -2
- package/src/ui/components/primitives/item.tsx +3 -1
- package/src/ui/components/primitives/menu.tsx +1 -7
- package/src/ui/components/primitives/pagination.tsx +16 -8
- package/src/ui/components/primitives/progress.tsx +32 -3
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/resizable.tsx +3 -1
- package/src/ui/components/primitives/select.tsx +6 -6
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +3 -0
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +5 -1
- package/src/ui/components/primitives/tabs.tsx +6 -3
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/components/primitives/tooltip.tsx +1 -0
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form.md +36 -3
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +13 -2
- package/src/ui/docs/content/action-trigger.md +12 -5
- package/src/ui/docs/content/action-view.md +11 -3
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +30 -14
- package/src/ui/docs/content/calendar.md +13 -0
- package/src/ui/docs/content/card.md +26 -0
- package/src/ui/docs/content/chat.md +20 -0
- package/src/ui/docs/content/cli.md +71 -19
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/composer.md +15 -0
- package/src/ui/docs/content/content.md +17 -1
- package/src/ui/docs/content/copyable.md +8 -0
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/detail.md +19 -1
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/dictionary-value.md +9 -2
- package/src/ui/docs/content/dock.md +8 -0
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/empty.md +2 -2
- package/src/ui/docs/content/getting-started.md +2 -2
- package/src/ui/docs/content/icon-picker.md +11 -0
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/label.md +7 -0
- package/src/ui/docs/content/menu.md +6 -0
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/page.md +20 -4
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/popover.md +6 -0
- package/src/ui/docs/content/progress.md +8 -11
- package/src/ui/docs/content/select.md +5 -5
- package/src/ui/docs/content/semantic-context.md +5 -4
- package/src/ui/docs/content/sidebar.md +13 -47
- package/src/ui/docs/content/skeleton.md +6 -0
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/split.md +21 -0
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/textarea.md +7 -0
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/docs/content/tokens.md +4 -4
- package/src/ui/docs/content/truncate.md +8 -0
- package/src/ui/docs/content/ui.md +14 -0
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +5 -5
- package/src/ui/docs/standalone.tsx +2 -2
- package/src/ui/drivers/react.tsx +12 -12
- package/src/ui/lib/action-errors.ts +45 -0
- package/src/ui/lib/zod-pt-br.ts +31 -4
- package/src/ui/meta.ts +8 -8
- package/src/ui/react.tsx +10 -16
- package/src/ui/theme.css +10 -8
- package/src/vite/design.ts +6 -18
- package/src/ui/components/patterns/shell-nav.tsx +0 -147
package/src/core/contracts.ts
CHANGED
|
@@ -64,6 +64,7 @@ export type ViewContract<In = unknown, Out = unknown, ParsedIn = any> = Omit<
|
|
|
64
64
|
BindingKeys
|
|
65
65
|
>
|
|
66
66
|
|
|
67
|
+
/** União dos contratos por kind — o que `bindAction` aceita e a UI tipada consome. */
|
|
67
68
|
export type ActionContract<In = any, Out = any, ParsedIn = any> =
|
|
68
69
|
| SimpleContract<In, Out, ParsedIn>
|
|
69
70
|
| FormContract<In & Record<string, unknown>, Out, ParsedIn>
|
|
@@ -97,6 +98,12 @@ export interface ActionBinding<ParsedIn, Out> {
|
|
|
97
98
|
// defineContract (overloads por kind, igual defineAction)
|
|
98
99
|
// =============================================================================
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Declara a parte compartilhável de uma action (identidade, schemas, docs e
|
|
103
|
+
* `authorize` action-level) sem handler nem dependência server-only — pode
|
|
104
|
+
* ser importada pelo cliente. Identity function tipada; o kind é inferido
|
|
105
|
+
* pelo overload. Complete com `bindAction` no servidor.
|
|
106
|
+
*/
|
|
100
107
|
export function defineContract<In, Out, ParsedIn = In>(
|
|
101
108
|
contract: SimpleContract<In, Out, ParsedIn>,
|
|
102
109
|
): SimpleContract<In, Out, ParsedIn>
|
package/src/core/domain.ts
CHANGED
package/src/core/errors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Opus — Error factory
|
|
3
3
|
*
|
|
4
4
|
* `error()` é a maneira canônica de emitir `ActionError` de dentro de um handler.
|
|
5
5
|
* Aplica defaults sensatos baseados na categoria (severity, retriable) e
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* throw error({
|
|
10
10
|
* code: 'deal.archive.notFound',
|
|
11
11
|
* category: 'not_found',
|
|
12
|
-
* message: `
|
|
12
|
+
* message: `Negócio ${id} não encontrado`,
|
|
13
13
|
* })
|
|
14
14
|
*
|
|
15
15
|
* Erros não criados via `error()` (ex: `throw new Error(...)`, exceções de driver)
|
|
16
|
-
* são normalizados pelo runtime em `{ category: 'internal', code: 'internal.unhandled' }
|
|
16
|
+
* são normalizados pelo runtime em `{ category: 'internal', code: 'internal.unhandled' }`
|
|
17
|
+
* com a microcopy fixa `UNHANDLED_ERROR_MESSAGE`; o texto original vai só para `cause`.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import type {
|
|
@@ -24,7 +25,7 @@ import type {
|
|
|
24
25
|
} from './types.ts'
|
|
25
26
|
|
|
26
27
|
/** Brand interno pra distinguir ActionError construído via `error()` de Error genérico. */
|
|
27
|
-
const ACTION_ERROR_BRAND = Symbol('
|
|
28
|
+
const ACTION_ERROR_BRAND = Symbol('opus.ActionError')
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Defaults de severity por categoria.
|
|
@@ -148,27 +149,29 @@ export function isActionError(value: unknown): value is ActionError {
|
|
|
148
149
|
)
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Microcopy que a pessoa recebe quando um handler falha com exceção não tratada.
|
|
154
|
+
* Fixa de propósito: a mensagem crua (driver, SQL, stack) fica no logger do
|
|
155
|
+
* runtime e em `cause`, nunca vira o texto exibido.
|
|
156
|
+
*/
|
|
157
|
+
export const UNHANDLED_ERROR_MESSAGE =
|
|
158
|
+
'Não foi possível concluir a operação. Tente novamente.'
|
|
159
|
+
|
|
151
160
|
/**
|
|
152
161
|
* Normaliza uma exceção qualquer em ActionError. Usado pelo runtime quando
|
|
153
162
|
* handler lança algo que não foi criado via `error()`.
|
|
163
|
+
*
|
|
164
|
+
* A `message` é sempre `UNHANDLED_ERROR_MESSAGE`; o valor lançado é preservado
|
|
165
|
+
* em `cause` (só a mensagem quando for `Error` — stack não vaza pra wire).
|
|
154
166
|
*/
|
|
155
167
|
export function normalizeError(thrown: unknown): ActionError {
|
|
156
168
|
if (isActionError(thrown)) return thrown
|
|
157
169
|
|
|
158
|
-
if (thrown instanceof Error) {
|
|
159
|
-
return error({
|
|
160
|
-
code: 'internal.unhandled',
|
|
161
|
-
category: 'internal',
|
|
162
|
-
message: thrown.message,
|
|
163
|
-
cause: thrown.message, // só mensagem; stack não vaza pra wire
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
170
|
return error({
|
|
168
171
|
code: 'internal.unhandled',
|
|
169
172
|
category: 'internal',
|
|
170
|
-
message:
|
|
171
|
-
cause: thrown,
|
|
173
|
+
message: UNHANDLED_ERROR_MESSAGE,
|
|
174
|
+
cause: thrown instanceof Error ? thrown.message : thrown,
|
|
172
175
|
})
|
|
173
176
|
}
|
|
174
177
|
|
package/src/core/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @softize/opus/core — public entry
|
|
3
3
|
*
|
|
4
|
-
* Re-exports da superfície pública.
|
|
4
|
+
* Re-exports da superfície pública. A fonte de verdade são as declarações
|
|
5
|
+
* (`defineContract`, `bindAction`, `defineDomain`…); `/docs/protocol.md`, o
|
|
6
|
+
* manifest e o OpenAPI são projeções delas.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
// — Tipos —————————————————————————————————————————————————————————————————————
|
|
@@ -128,7 +130,7 @@ export type {
|
|
|
128
130
|
} from './types.ts'
|
|
129
131
|
|
|
130
132
|
// — Erro ——————————————————————————————————————————————————————————————————————
|
|
131
|
-
export { error, isActionError, normalizeError } from './errors.ts'
|
|
133
|
+
export { error, isActionError, normalizeError, UNHANDLED_ERROR_MESSAGE } from './errors.ts'
|
|
132
134
|
export type { ErrorInput } from './errors.ts'
|
|
133
135
|
export { normalizeTraceContext } from './trace.ts'
|
|
134
136
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opus — versão do pacote lida do `package.json` que viaja junto do source.
|
|
3
|
+
*
|
|
4
|
+
* Server-only (usa `node:fs`); não é re-exportado por `core/index.ts`. Usado
|
|
5
|
+
* pelos drivers de servidor (info do OpenAPI), pelo servidor MCP e pelo plugin
|
|
6
|
+
* de design pra identificar a versão do SDK sem duplicar leitura de arquivo.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
|
|
11
|
+
const FALLBACK_VERSION = '0.0.0'
|
|
12
|
+
|
|
13
|
+
/** Versão de `@softize/opus` resolvida do `package.json` real; `0.0.0` se a leitura falhar. */
|
|
14
|
+
export function readPackageVersion(): string {
|
|
15
|
+
// `import.meta.url` sobrevive ao bundling do vite.config (o vite injeta a
|
|
16
|
+
// URL original do arquivo), então o package.json resolve do source real.
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(
|
|
19
|
+
readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),
|
|
20
|
+
) as { version?: string }
|
|
21
|
+
return pkg.version ?? FALLBACK_VERSION
|
|
22
|
+
/* v8 ignore next 3 — defensivo; o package.json viaja junto do source */
|
|
23
|
+
} catch {
|
|
24
|
+
return FALLBACK_VERSION
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/core/reactions.ts
CHANGED
package/src/core/runtime.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Opus — Runtime
|
|
3
3
|
*
|
|
4
4
|
* Orquestrador central. Recebe actions + reactions + adapters + config no setup,
|
|
5
5
|
* registra cada peça, e executa o pipeline padrão (validate → load → auth →
|
|
@@ -281,7 +281,7 @@ export class Runtime {
|
|
|
281
281
|
this.config = applyConfigDefaults(setup.config)
|
|
282
282
|
this.logger = setup.logger
|
|
283
283
|
this.observability = setup.observability
|
|
284
|
-
this.log = setup.logger ?? new ConsoleLogger({ runtime: '
|
|
284
|
+
this.log = setup.logger ?? new ConsoleLogger({ runtime: 'opus' })
|
|
285
285
|
|
|
286
286
|
this.audit = new AuditEmitter({ mode: this.config.auditMode, log: this.log })
|
|
287
287
|
if (setup.audit !== undefined) {
|
|
@@ -345,7 +345,7 @@ export class Runtime {
|
|
|
345
345
|
throw error({
|
|
346
346
|
code: 'runtime.duplicate_loader',
|
|
347
347
|
category: 'internal',
|
|
348
|
-
message: `
|
|
348
|
+
message: `Já existe um resolver de loader registrado para "${entity}"`,
|
|
349
349
|
})
|
|
350
350
|
}
|
|
351
351
|
this.loaderResolvers.set(entity, resolver)
|
|
@@ -368,7 +368,7 @@ export class Runtime {
|
|
|
368
368
|
throw error({
|
|
369
369
|
code: 'runtime.duplicate_action',
|
|
370
370
|
category: 'internal',
|
|
371
|
-
message: `Action "${action.name}"
|
|
371
|
+
message: `Action "${action.name}" registrada duas vezes`,
|
|
372
372
|
})
|
|
373
373
|
}
|
|
374
374
|
this.actions.set(action.name, action)
|
|
@@ -381,7 +381,7 @@ export class Runtime {
|
|
|
381
381
|
throw error({
|
|
382
382
|
code: 'runtime.duplicate_reaction',
|
|
383
383
|
category: 'internal',
|
|
384
|
-
message: `Reaction "${reaction.name}"
|
|
384
|
+
message: `Reaction "${reaction.name}" registrada duas vezes`,
|
|
385
385
|
})
|
|
386
386
|
}
|
|
387
387
|
this.reactions.set(reaction.name, reaction)
|
|
@@ -394,7 +394,7 @@ export class Runtime {
|
|
|
394
394
|
throw error({
|
|
395
395
|
code: 'runtime.duplicate_schedule',
|
|
396
396
|
category: 'internal',
|
|
397
|
-
message: `Schedule "${schedule.name}"
|
|
397
|
+
message: `Schedule "${schedule.name}" registrado duas vezes`,
|
|
398
398
|
})
|
|
399
399
|
}
|
|
400
400
|
this.schedules.set(schedule.name, schedule)
|
|
@@ -410,7 +410,7 @@ export class Runtime {
|
|
|
410
410
|
throw error({
|
|
411
411
|
code: 'runtime.already_started',
|
|
412
412
|
category: 'internal',
|
|
413
|
-
message: 'Runtime.start()
|
|
413
|
+
message: 'Runtime.start() foi chamado mais de uma vez',
|
|
414
414
|
})
|
|
415
415
|
}
|
|
416
416
|
|
|
@@ -431,7 +431,7 @@ export class Runtime {
|
|
|
431
431
|
throw error({
|
|
432
432
|
code: 'runtime.eventbus_required',
|
|
433
433
|
category: 'internal',
|
|
434
|
-
message: `${this.reactions.size} reaction(s)
|
|
434
|
+
message: `${this.reactions.size} reaction(s) registrada(s) sem um EventBusAdapter configurado`,
|
|
435
435
|
})
|
|
436
436
|
}
|
|
437
437
|
|
|
@@ -441,7 +441,7 @@ export class Runtime {
|
|
|
441
441
|
throw error({
|
|
442
442
|
code: 'runtime.scheduler_required',
|
|
443
443
|
category: 'internal',
|
|
444
|
-
message: `${this.schedules.size} schedule(s)
|
|
444
|
+
message: `${this.schedules.size} schedule(s) registrado(s) sem um SchedulerAdapter configurado`,
|
|
445
445
|
})
|
|
446
446
|
}
|
|
447
447
|
|
|
@@ -514,7 +514,7 @@ export class Runtime {
|
|
|
514
514
|
return this.errorResult(actionName, error({
|
|
515
515
|
code: 'runtime.action_not_found',
|
|
516
516
|
category: 'not_found',
|
|
517
|
-
message: `Action "${actionName}"
|
|
517
|
+
message: `Action "${actionName}" não está registrada`,
|
|
518
518
|
}), 0, ctxBase.requestId)
|
|
519
519
|
}
|
|
520
520
|
|
|
@@ -535,21 +535,21 @@ export class Runtime {
|
|
|
535
535
|
return this.errorResult(spec.action, error({
|
|
536
536
|
code: 'runtime.action_not_found',
|
|
537
537
|
category: 'not_found',
|
|
538
|
-
message: `Action "${spec.action}"
|
|
538
|
+
message: `Action "${spec.action}" não está registrada`,
|
|
539
539
|
}), 0, spec.ctx.requestId)
|
|
540
540
|
}
|
|
541
541
|
if (!isBackgroundAction(action)) {
|
|
542
542
|
return this.errorResult(spec.action, error({
|
|
543
543
|
code: 'runtime.action_not_background',
|
|
544
544
|
category: 'validation',
|
|
545
|
-
message: `Action "${spec.action}"
|
|
545
|
+
message: `Action "${spec.action}" não está declarada como background`,
|
|
546
546
|
}), 0, spec.ctx.requestId)
|
|
547
547
|
}
|
|
548
548
|
if ((ctxBase.user?.id ?? null) !== spec.ctx.userId) {
|
|
549
549
|
return this.errorResult(spec.action, error({
|
|
550
550
|
code: 'runtime.job_actor_mismatch',
|
|
551
551
|
category: 'authentication',
|
|
552
|
-
message: '
|
|
552
|
+
message: 'O ator do contexto do worker não corresponde ao envelope do job',
|
|
553
553
|
}), 0, spec.ctx.requestId)
|
|
554
554
|
}
|
|
555
555
|
|
|
@@ -604,7 +604,7 @@ export class Runtime {
|
|
|
604
604
|
throw error({
|
|
605
605
|
code: 'auth.unauthenticated',
|
|
606
606
|
category: 'authentication',
|
|
607
|
-
message: '
|
|
607
|
+
message: 'Autenticação necessária',
|
|
608
608
|
})
|
|
609
609
|
}
|
|
610
610
|
|
|
@@ -616,7 +616,7 @@ export class Runtime {
|
|
|
616
616
|
throw error({
|
|
617
617
|
code: 'auth.forbidden',
|
|
618
618
|
category: 'authorization',
|
|
619
|
-
message: '
|
|
619
|
+
message: 'Acesso negado',
|
|
620
620
|
})
|
|
621
621
|
}
|
|
622
622
|
if (decision !== true) throw decision
|
|
@@ -627,7 +627,7 @@ export class Runtime {
|
|
|
627
627
|
throw error({
|
|
628
628
|
code: 'runtime.queue_required',
|
|
629
629
|
category: 'internal',
|
|
630
|
-
message: `
|
|
630
|
+
message: `A action background "${action.name}" exige um QueueAdapter`,
|
|
631
631
|
})
|
|
632
632
|
}
|
|
633
633
|
const background = action.background
|
|
@@ -635,7 +635,7 @@ export class Runtime {
|
|
|
635
635
|
throw error({
|
|
636
636
|
code: 'runtime.invalid_background_config',
|
|
637
637
|
category: 'internal',
|
|
638
|
-
message: `
|
|
638
|
+
message: `A action background "${action.name}" não tem configuração de background`,
|
|
639
639
|
})
|
|
640
640
|
}
|
|
641
641
|
const spec: JobSpec = {
|
|
@@ -729,6 +729,14 @@ export class Runtime {
|
|
|
729
729
|
meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId, trace),
|
|
730
730
|
}
|
|
731
731
|
} catch (thrown) {
|
|
732
|
+
// Exceção fora de `error()` vira microcopy fixa no envelope; o texto
|
|
733
|
+
// original só sobrevive aqui, no logger da action, e em `cause`.
|
|
734
|
+
if (!isActionError(thrown)) {
|
|
735
|
+
ctx.log.error('action failed with unhandled error', {
|
|
736
|
+
error: thrown instanceof Error ? thrown.message : String(thrown),
|
|
737
|
+
...(thrown instanceof Error && thrown.stack !== undefined ? { stack: thrown.stack } : {}),
|
|
738
|
+
})
|
|
739
|
+
}
|
|
732
740
|
const actionError = normalizeError(thrown)
|
|
733
741
|
const durationMs = performance.now() - startedAt
|
|
734
742
|
await this.audit.emit(
|
|
@@ -898,7 +906,7 @@ export class Runtime {
|
|
|
898
906
|
throw error({
|
|
899
907
|
code: 'emit.no_bus',
|
|
900
908
|
category: 'internal',
|
|
901
|
-
message: `ctx.emit('${event}')
|
|
909
|
+
message: `ctx.emit('${event}') chamado sem EventBusAdapter configurado`,
|
|
902
910
|
})
|
|
903
911
|
}
|
|
904
912
|
this.log.warn('emit dropped — no EventBusAdapter', { event })
|
|
@@ -931,7 +939,7 @@ export class Runtime {
|
|
|
931
939
|
throw error({
|
|
932
940
|
code: 'emit.failed',
|
|
933
941
|
category: 'internal',
|
|
934
|
-
message: `
|
|
942
|
+
message: `Falha ao publicar o evento "${event}"`,
|
|
935
943
|
cause: String(err),
|
|
936
944
|
})
|
|
937
945
|
}
|
|
@@ -953,7 +961,7 @@ export class Runtime {
|
|
|
953
961
|
throw error({
|
|
954
962
|
code: kind === 'input' ? 'validation.invalid_input' : 'validation.invalid_output',
|
|
955
963
|
category: kind === 'input' ? 'validation' : 'internal',
|
|
956
|
-
message: kind === 'input' ? '
|
|
964
|
+
message: kind === 'input' ? 'Dados de entrada inválidos' : 'Dados de saída inválidos (validação em dev)',
|
|
957
965
|
issues: result.issues.map((iss) => ({
|
|
958
966
|
path: (iss.path ?? []).join('.'),
|
|
959
967
|
code: 'invalid',
|
|
@@ -978,7 +986,7 @@ export class Runtime {
|
|
|
978
986
|
throw error({
|
|
979
987
|
code: `${action.name}.${key}.not_found`,
|
|
980
988
|
category: 'not_found',
|
|
981
|
-
message: `
|
|
989
|
+
message: `Recurso "${key}" não encontrado para a action "${action.name}"`,
|
|
982
990
|
})
|
|
983
991
|
}
|
|
984
992
|
loaded[key] = value
|
|
@@ -1128,7 +1136,7 @@ export class Runtime {
|
|
|
1128
1136
|
throw error({
|
|
1129
1137
|
code: 'runtime.subscribe_unsupported',
|
|
1130
1138
|
category: 'internal',
|
|
1131
|
-
message: 'EventBusAdapter
|
|
1139
|
+
message: 'O EventBusAdapter não suporta subscribe()',
|
|
1132
1140
|
})
|
|
1133
1141
|
}
|
|
1134
1142
|
const patterns = Array.isArray(reaction.on) ? reaction.on : [reaction.on]
|
|
@@ -1222,10 +1230,12 @@ export class Runtime {
|
|
|
1222
1230
|
},
|
|
1223
1231
|
)
|
|
1224
1232
|
} catch (err) {
|
|
1225
|
-
const actionError =
|
|
1233
|
+
const actionError = normalizeError(err)
|
|
1226
1234
|
reactionLog.error('reaction failed', {
|
|
1227
1235
|
code: actionError.code,
|
|
1228
1236
|
message: actionError.message,
|
|
1237
|
+
// Erro não tratado tem microcopy fixa; o texto original está em `cause`.
|
|
1238
|
+
...(actionError.cause !== undefined ? { cause: actionError.cause } : {}),
|
|
1229
1239
|
})
|
|
1230
1240
|
}
|
|
1231
1241
|
}
|
package/src/core/schedules.ts
CHANGED
package/src/core/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Opus — Core types
|
|
3
3
|
*
|
|
4
4
|
* Tipos centrais do protocolo. Source of truth: `/docs/protocol.md`.
|
|
5
5
|
* Qualquer divergência entre este arquivo e a doc é bug; alinhar a doc primeiro,
|
|
@@ -250,7 +250,7 @@ export interface User {
|
|
|
250
250
|
|
|
251
251
|
/**
|
|
252
252
|
* Função que checa permissão. Plugada pelo `AuthAdapter`.
|
|
253
|
-
*
|
|
253
|
+
* O Opus não implementa RBAC/ABAC — apenas delega.
|
|
254
254
|
*/
|
|
255
255
|
export type CanFn = (permission: string, resource?: unknown) => boolean | Promise<boolean>
|
|
256
256
|
|
|
@@ -551,9 +551,8 @@ export type FieldWidget = BuiltInFieldWidget | (string & { readonly __opusCustom
|
|
|
551
551
|
export interface FieldSpec {
|
|
552
552
|
label: I18nRef
|
|
553
553
|
placeholder?: I18nRef
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
* `help` = explicação mais longa, escondida atrás do ícone na label. */
|
|
554
|
+
/** Ajuda junto à label (ícone ⓘ + tooltip): critério, efeito ou limitação que a label não
|
|
555
|
+
* diz. Ajuda que só repete a label deve ser omitida. */
|
|
557
556
|
help?: I18nRef
|
|
558
557
|
|
|
559
558
|
default?: unknown
|
|
@@ -629,7 +628,7 @@ export interface ListColumnSpec {
|
|
|
629
628
|
* 'badge' (chip `outline` com o valor — legado; coluna de dicionário com `presentation`
|
|
630
629
|
* declarado usa `DictionaryValue` e dispensa este tipo). */
|
|
631
630
|
type?: 'text' | 'number' | 'date' | 'badge'
|
|
632
|
-
/** Referência do dicionário registrado em `
|
|
631
|
+
/** Referência do dicionário registrado em `OpusProvider dicts` que esta coluna mostra,
|
|
633
632
|
* quando o schema de saída não carrega a meta de `t.dict` (ex.: campo `z.string()`).
|
|
634
633
|
* Coluna cujo campo de saída É um `t.dict().zod()` resolve sozinha, sem esta chave. */
|
|
635
634
|
dictionary?: string
|
package/src/dsl/eval.ts
CHANGED
|
@@ -64,7 +64,7 @@ export function evalExpression(node: AstNode, ctx: EvalContext): unknown {
|
|
|
64
64
|
return (left as number) / (right as number)
|
|
65
65
|
/* v8 ignore next 2 */
|
|
66
66
|
default:
|
|
67
|
-
throw new Error(`DSL eval:
|
|
67
|
+
throw new Error(`DSL eval: operador binário desconhecido ${node.op as string}`)
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
case 'in': {
|
|
@@ -132,5 +132,5 @@ function callFunction(name: string, args: unknown[]): unknown {
|
|
|
132
132
|
}
|
|
133
133
|
return null
|
|
134
134
|
}
|
|
135
|
-
throw new Error(`DSL eval:
|
|
135
|
+
throw new Error(`DSL eval: função desconhecida '${name}'`)
|
|
136
136
|
}
|
package/src/dsl/kysely.ts
CHANGED
|
@@ -136,7 +136,7 @@ function compile(node: AstNode, eb: KyselyEb, bindings: KyselyBindings): any {
|
|
|
136
136
|
return (left as number) / (right as number)
|
|
137
137
|
/* v8 ignore next 2 */
|
|
138
138
|
default:
|
|
139
|
-
throw new Error(`DSL kysely:
|
|
139
|
+
throw new Error(`DSL kysely: operador binário não tratado ${node.op as string}`)
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
case 'in': {
|
|
@@ -195,7 +195,7 @@ function compile(node: AstNode, eb: KyselyEb, bindings: KyselyBindings): any {
|
|
|
195
195
|
`DSL kysely: function '${node.name}' precisa de mapping de coluna explícito (use eb.fn)`,
|
|
196
196
|
)
|
|
197
197
|
}
|
|
198
|
-
throw new Error(`DSL kysely:
|
|
198
|
+
throw new Error(`DSL kysely: função desconhecida '${node.name}'`)
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
}
|
package/src/dsl/loads.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function parseLoad(src: string): LoadSpec {
|
|
|
31
31
|
const trimmed = src.trim()
|
|
32
32
|
const m = LOAD_RE.exec(trimmed)
|
|
33
33
|
if (m === null) {
|
|
34
|
-
throw new Error(`DSL loads:
|
|
34
|
+
throw new Error(`DSL loads: sintaxe inválida '${src}'`)
|
|
35
35
|
}
|
|
36
36
|
const entity = m[1]!
|
|
37
37
|
const argsRaw = m[2]!.trim()
|
package/src/dsl/parser.ts
CHANGED
|
@@ -89,7 +89,7 @@ function tokenize(src: string): Token[] {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
if (i >= src.length) {
|
|
92
|
-
throw new Error(`DSL parse
|
|
92
|
+
throw new Error(`DSL parse: string não terminada na posição ${startPos}`)
|
|
93
93
|
}
|
|
94
94
|
i += 1 // skip closing quote
|
|
95
95
|
tokens.push({ type: 'STRING', value: val, pos: startPos })
|
|
@@ -150,7 +150,7 @@ function tokenize(src: string): Token[] {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
throw new Error(
|
|
153
|
-
`DSL parse
|
|
153
|
+
`DSL parse: caractere inesperado '${c}' na posição ${i}`,
|
|
154
154
|
)
|
|
155
155
|
}
|
|
156
156
|
tokens.push({ type: 'EOF', value: '', pos: src.length })
|
|
@@ -184,7 +184,7 @@ class Parser {
|
|
|
184
184
|
if (!this.match(type, value)) {
|
|
185
185
|
const t = this.peek()
|
|
186
186
|
throw new Error(
|
|
187
|
-
`DSL parse
|
|
187
|
+
`DSL parse: esperado ${value ?? type}, recebido '${t.value}' na posição ${t.pos}`,
|
|
188
188
|
)
|
|
189
189
|
}
|
|
190
190
|
return this.consume()
|
|
@@ -343,7 +343,7 @@ class Parser {
|
|
|
343
343
|
const t = this.consume()
|
|
344
344
|
const n = Number(t.value)
|
|
345
345
|
if (Number.isNaN(n)) {
|
|
346
|
-
throw new Error(`DSL parse
|
|
346
|
+
throw new Error(`DSL parse: número inválido '${t.value}'`)
|
|
347
347
|
}
|
|
348
348
|
return { kind: 'literal', value: n }
|
|
349
349
|
}
|
|
@@ -409,7 +409,7 @@ class Parser {
|
|
|
409
409
|
}
|
|
410
410
|
const t = this.peek()
|
|
411
411
|
throw new Error(
|
|
412
|
-
`DSL parse
|
|
412
|
+
`DSL parse: token inesperado '${t.value}' na posição ${t.pos}`,
|
|
413
413
|
)
|
|
414
414
|
}
|
|
415
415
|
}
|
|
@@ -39,6 +39,7 @@ export interface MittEventsOptions {
|
|
|
39
39
|
*/
|
|
40
40
|
type EventMap = Record<string, DomainEvent>
|
|
41
41
|
|
|
42
|
+
/** EventBusAdapter in-process baseado em `mitt`; entrega no mesmo processo, sem persistência. */
|
|
42
43
|
export function mittEvents(options: MittEventsOptions = {}): EventBusAdapter {
|
|
43
44
|
const { name = 'mitt' } = options
|
|
44
45
|
const emitter: Emitter<EventMap> = mitt<EventMap>()
|
package/src/mcp/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
|
13
13
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
14
14
|
import { zodToJsonSchema } from 'zod-to-json-schema'
|
|
15
15
|
import type { ContextBase, Runtime } from '../core/index.ts'
|
|
16
|
+
import { readPackageVersion } from '../core/package-version.ts'
|
|
16
17
|
|
|
17
18
|
export interface OpusMcpOptions {
|
|
18
19
|
name?: string
|
|
@@ -36,7 +37,7 @@ export function createOpusMcpServer(runtime: Runtime, opts: OpusMcpOptions = {})
|
|
|
36
37
|
opts.toJsonSchema ?? ((schema: unknown) => zodToJsonSchema(schema as never, { $refStrategy: 'none' }))
|
|
37
38
|
|
|
38
39
|
const server = new Server(
|
|
39
|
-
{ name: opts.name ?? 'opus', version: opts.version ??
|
|
40
|
+
{ name: opts.name ?? 'opus', version: opts.version ?? readPackageVersion() },
|
|
40
41
|
{ capabilities: { tools: {} } },
|
|
41
42
|
)
|
|
42
43
|
|
|
@@ -40,6 +40,7 @@ export interface OpenTelemetryObservabilityOptions {
|
|
|
40
40
|
healthCheck?: () => Promise<HealthStatus> | HealthStatus
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** ObservabilityAdapter que abre um span OpenTelemetry por action/reaction e propaga o trace context. */
|
|
43
44
|
export function openTelemetryObservability(
|
|
44
45
|
options: OpenTelemetryObservabilityOptions = {},
|
|
45
46
|
): ObservabilityAdapter {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* import { Queue } from 'bullmq'
|
|
14
14
|
* import { bullmqQueue } from '@softize/opus/queue/bullmq'
|
|
15
15
|
*
|
|
16
|
-
* const queue = new Queue('
|
|
16
|
+
* const queue = new Queue('opus-jobs', { connection: redisOpts })
|
|
17
17
|
* createRuntime({ queue: bullmqQueue({ queue }) })
|
|
18
18
|
*/
|
|
19
19
|
|
|
@@ -139,9 +139,9 @@ function bullmqOptions(spec: JobSpec): BullMQAddOptions {
|
|
|
139
139
|
// =============================================================================
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
* BullMQ state →
|
|
142
|
+
* BullMQ state → Opus JobStatus.
|
|
143
143
|
*
|
|
144
|
-
*
|
|
144
|
+
* O Opus não tem 'paused' nem 'delayed' separados — todos viram 'queued'.
|
|
145
145
|
* Distinção fina fica no `details.meta.bullmqState` se consumer precisar.
|
|
146
146
|
*/
|
|
147
147
|
export function mapState(state: BullMQState): JobStatus {
|
|
@@ -31,6 +31,7 @@ export interface NodeCronOptions {
|
|
|
31
31
|
onError?: (err: unknown, schedule: ScheduleDef) => void
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** SchedulerAdapter sobre `node-cron`; aceita `cron` ou o atalho `every` de cada schedule. */
|
|
34
35
|
export function nodeCronScheduler(
|
|
35
36
|
options: NodeCronOptions = {},
|
|
36
37
|
): SchedulerAdapter {
|
|
@@ -83,11 +84,11 @@ function resolveCron(spec: ScheduleDef): string {
|
|
|
83
84
|
if (spec.cron !== undefined) return spec.cron
|
|
84
85
|
if (spec.every !== undefined) return everyToCron(spec.every)
|
|
85
86
|
throw new Error(
|
|
86
|
-
`
|
|
87
|
+
`O schedule "${spec.name}" precisa declarar 'cron' ou 'every'.`,
|
|
87
88
|
)
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
function defaultOnError(err: unknown, schedule: ScheduleDef): void {
|
|
91
92
|
// eslint-disable-next-line no-console
|
|
92
|
-
console.error(`[
|
|
93
|
+
console.error(`[opus/scheduler] falha ao disparar o schedule "${schedule.name}":`, err)
|
|
93
94
|
}
|
package/src/scheduler/every.ts
CHANGED
|
@@ -15,30 +15,30 @@ export function everyToCron(every: string): string {
|
|
|
15
15
|
const match = /^(\d+)(s|m|h|d)$/.exec(every.trim())
|
|
16
16
|
if (match === null) {
|
|
17
17
|
throw new Error(
|
|
18
|
-
`
|
|
19
|
-
`
|
|
18
|
+
`Atalho 'every' inválido: "${every}". ` +
|
|
19
|
+
`Formato esperado "<n><s|m|h|d>" (ex: "15s", "5m", "1h", "1d").`,
|
|
20
20
|
)
|
|
21
21
|
}
|
|
22
22
|
const n = Number.parseInt(match[1] as string, 10)
|
|
23
23
|
const unit = match[2] as 's' | 'm' | 'h' | 'd'
|
|
24
24
|
|
|
25
25
|
if (n <= 0) {
|
|
26
|
-
throw new Error(`'every'
|
|
26
|
+
throw new Error(`O valor de 'every' precisa ser maior que 0; recebido "${every}".`)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
switch (unit) {
|
|
30
30
|
case 's':
|
|
31
|
-
if (n > 59) throw new Error(`'every'
|
|
31
|
+
if (n > 59) throw new Error(`Segundos em 'every' precisam ser no máximo 59; recebido ${n}.`)
|
|
32
32
|
return `*/${n} * * * * *`
|
|
33
33
|
case 'm':
|
|
34
|
-
if (n > 59) throw new Error(`'every'
|
|
34
|
+
if (n > 59) throw new Error(`Minutos em 'every' precisam ser no máximo 59; recebido ${n}.`)
|
|
35
35
|
return `*/${n} * * * *`
|
|
36
36
|
case 'h':
|
|
37
|
-
if (n > 23) throw new Error(`'every'
|
|
37
|
+
if (n > 23) throw new Error(`Horas em 'every' precisam ser no máximo 23; recebido ${n}.`)
|
|
38
38
|
return n === 1 ? '0 * * * *' : `0 */${n} * * *`
|
|
39
39
|
case 'd':
|
|
40
40
|
if (n !== 1) {
|
|
41
|
-
throw new Error(`'every'
|
|
41
|
+
throw new Error(`Dias em 'every' aceitam somente 1d. Para intervalos maiores, use cron.`)
|
|
42
42
|
}
|
|
43
43
|
return '0 0 * * *'
|
|
44
44
|
}
|
package/src/schema/openapi.ts
CHANGED
|
@@ -175,15 +175,15 @@ function buildResponses(
|
|
|
175
175
|
const successStatus = String(action.successStatus ?? 200)
|
|
176
176
|
const responses: Record<string, OpenAPIResponse> = {
|
|
177
177
|
[successStatus]: {
|
|
178
|
-
description: '
|
|
178
|
+
description: 'Sucesso',
|
|
179
179
|
content: {
|
|
180
180
|
'application/json': {
|
|
181
181
|
schema: wrapEnvelope(toJsonSchema(action.output), action.kind),
|
|
182
182
|
},
|
|
183
183
|
},
|
|
184
184
|
},
|
|
185
|
-
'4XX': errorResponse('
|
|
186
|
-
'5XX': errorResponse('
|
|
185
|
+
'4XX': errorResponse('Erro do cliente', action.errors),
|
|
186
|
+
'5XX': errorResponse('Erro do servidor'),
|
|
187
187
|
}
|
|
188
188
|
return responses
|
|
189
189
|
}
|