@softize/opus 12.11.0 → 13.1.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 +76 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/check.mjs +2 -7
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/copy.mjs +1 -5
- 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/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
- package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
- package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/radius-scale.md +1 -1
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
- package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
- package/registry/templates/app/package.json +1 -1
- 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 +2 -2
- 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/confirm.tsx +142 -42
- package/src/ui/components/patterns/content-header.tsx +7 -1
- package/src/ui/components/patterns/data-state.tsx +1 -1
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +12 -8
- package/src/ui/components/patterns/list.tsx +36 -41
- package/src/ui/components/patterns/page-state.tsx +2 -2
- package/src/ui/components/patterns/page.tsx +19 -1
- package/src/ui/components/patterns/shell-nav.tsx +10 -3
- package/src/ui/components/patterns/sidebar.tsx +43 -32
- package/src/ui/components/patterns/trigger.tsx +39 -38
- package/src/ui/components/patterns/view.tsx +26 -17
- package/src/ui/components/primitives/alert.tsx +14 -8
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/badge.tsx +11 -6
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +16 -3
- 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 +12 -0
- package/src/ui/components/primitives/copyable.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +202 -40
- package/src/ui/components/primitives/dot.tsx +5 -0
- package/src/ui/components/primitives/drawer.tsx +18 -8
- package/src/ui/components/primitives/empty.tsx +3 -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 +1 -1
- 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 -3
- 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 +5 -5
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +190 -8
- package/src/ui/components/primitives/switch.tsx +1 -0
- package/src/ui/components/primitives/tabs.tsx +1 -0
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/components/primitives/tooltip.tsx +1 -0
- package/src/ui/docs/DocBrowser.tsx +102 -23
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/accordion.md +22 -16
- package/src/ui/docs/content/action-form-card.md +8 -8
- package/src/ui/docs/content/action-form-dialog.md +9 -9
- package/src/ui/docs/content/action-form.md +37 -36
- package/src/ui/docs/content/action-list-dialog.md +11 -6
- package/src/ui/docs/content/action-list.md +73 -39
- package/src/ui/docs/content/action-trigger.md +29 -15
- package/src/ui/docs/content/action-view.md +17 -10
- package/src/ui/docs/content/actions.md +9 -9
- package/src/ui/docs/content/ai.md +3 -3
- package/src/ui/docs/content/alert.md +14 -12
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/aspect-ratio.md +4 -4
- package/src/ui/docs/content/audit.md +2 -2
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/avatar.md +34 -14
- package/src/ui/docs/content/badge.md +3 -3
- package/src/ui/docs/content/breadcrumb.md +13 -8
- package/src/ui/docs/content/button.md +81 -6
- package/src/ui/docs/content/calendar.md +18 -5
- package/src/ui/docs/content/card.md +27 -1
- package/src/ui/docs/content/carousel.md +16 -11
- package/src/ui/docs/content/chat.md +23 -3
- package/src/ui/docs/content/checkbox.md +7 -7
- package/src/ui/docs/content/cli.md +74 -22
- package/src/ui/docs/content/collapsible.md +8 -8
- package/src/ui/docs/content/command.md +16 -8
- package/src/ui/docs/content/composer.md +17 -2
- package/src/ui/docs/content/content.md +17 -2
- package/src/ui/docs/content/copyable.md +12 -3
- package/src/ui/docs/content/customization.md +5 -5
- package/src/ui/docs/content/cycle.md +3 -3
- package/src/ui/docs/content/data-state.md +11 -12
- package/src/ui/docs/content/data.md +26 -33
- package/src/ui/docs/content/detail.md +22 -4
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +17 -10
- package/src/ui/docs/content/dock.md +11 -3
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/drawer.md +27 -14
- package/src/ui/docs/content/empty-value.md +2 -2
- package/src/ui/docs/content/empty.md +19 -12
- package/src/ui/docs/content/events.md +4 -4
- package/src/ui/docs/content/field.md +34 -12
- package/src/ui/docs/content/getting-started.md +3 -3
- package/src/ui/docs/content/icon-picker.md +19 -4
- package/src/ui/docs/content/input-otp.md +20 -12
- package/src/ui/docs/content/input.md +121 -9
- package/src/ui/docs/content/item.md +27 -13
- package/src/ui/docs/content/kbd.md +19 -11
- package/src/ui/docs/content/label.md +12 -3
- package/src/ui/docs/content/log.md +4 -4
- package/src/ui/docs/content/markdown.md +7 -6
- package/src/ui/docs/content/mcp.md +13 -15
- package/src/ui/docs/content/menu.md +40 -16
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +59 -6
- package/src/ui/docs/content/pagination.md +22 -17
- package/src/ui/docs/content/popover.md +22 -8
- package/src/ui/docs/content/progress.md +15 -16
- package/src/ui/docs/content/queue.md +5 -5
- package/src/ui/docs/content/radio-group.md +20 -12
- package/src/ui/docs/content/router.md +11 -6
- package/src/ui/docs/content/scheduler.md +4 -5
- package/src/ui/docs/content/scroll-area.md +12 -7
- package/src/ui/docs/content/select.md +47 -34
- package/src/ui/docs/content/semantic-context.md +2 -2
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +329 -54
- package/src/ui/docs/content/skeleton.md +9 -2
- package/src/ui/docs/content/slider.md +8 -7
- package/src/ui/docs/content/spinner.md +8 -8
- package/src/ui/docs/content/split.md +29 -5
- package/src/ui/docs/content/storage.md +6 -8
- package/src/ui/docs/content/switch.md +8 -7
- package/src/ui/docs/content/table.md +13 -3
- package/src/ui/docs/content/tabs.md +28 -14
- package/src/ui/docs/content/testing.md +9 -11
- package/src/ui/docs/content/textarea.md +12 -4
- package/src/ui/docs/content/toast.md +47 -13
- package/src/ui/docs/content/toggle.md +75 -7
- package/src/ui/docs/content/tokens.md +7 -7
- package/src/ui/docs/content/tooltip.md +19 -11
- package/src/ui/docs/content/truncate.md +15 -8
- package/src/ui/docs/content/ui.md +24 -9
- package/src/ui/docs/content/upgrading.md +7 -8
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +25 -42
- package/src/ui/docs/standalone.tsx +2 -2
- package/src/ui/drivers/react.tsx +17 -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 +65 -95
- package/src/ui/react.tsx +17 -16
- package/src/ui/theme.css +60 -8
- package/src/vite/design.ts +6 -18
- package/src/ui/components/primitives/alert-dialog.tsx +0 -192
- package/src/ui/docs/content/alert-dialog.md +0 -73
- package/src/ui/docs/content/button-group.md +0 -71
- package/src/ui/docs/content/confirm.md +0 -120
- package/src/ui/docs/content/input-group.md +0 -79
- package/src/ui/docs/content/page-state.md +0 -45
- package/src/ui/docs/content/toggle-group.md +0 -81
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
const repoRoot = resolve(process.argv[2] ?? process.cwd());
|
|
7
|
+
const contentDir = resolve(repoRoot, "packages/opus/src/ui/docs/content");
|
|
8
|
+
const registryPath = resolve(repoRoot, "packages/opus/src/ui/docs/registry.tsx");
|
|
9
|
+
|
|
10
|
+
if (!existsSync(contentDir) || !existsSync(registryPath)) {
|
|
11
|
+
console.error(
|
|
12
|
+
"Não encontrei packages/opus/src/ui/docs/content e registry.tsx. Passe a raiz do repositório Opus.",
|
|
13
|
+
);
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const registry = readFileSync(registryPath, "utf8");
|
|
18
|
+
const importedFiles = new Set(
|
|
19
|
+
[...registry.matchAll(/\.\/content\/([^"']+\.md)\?raw/g)].map((match) => match[1]),
|
|
20
|
+
);
|
|
21
|
+
const files = readdirSync(contentDir)
|
|
22
|
+
.filter((file) => file.endsWith(".md"))
|
|
23
|
+
.sort();
|
|
24
|
+
const findings = [];
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const source = readFileSync(resolve(contentDir, file), "utf8");
|
|
28
|
+
const lines = source.split("\n");
|
|
29
|
+
|
|
30
|
+
if (!importedFiles.has(file)) {
|
|
31
|
+
findings.push(`${file}: página órfã; não há import no registry`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let inFence = false;
|
|
35
|
+
let previousHeading = 0;
|
|
36
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
37
|
+
const line = lines[index];
|
|
38
|
+
const lineNumber = index + 1;
|
|
39
|
+
|
|
40
|
+
if (/^```/.test(line)) {
|
|
41
|
+
inFence = !inFence;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (inFence) continue;
|
|
45
|
+
|
|
46
|
+
const heading = /^(#{1,6})\s+/.exec(line);
|
|
47
|
+
if (heading) {
|
|
48
|
+
const level = heading[1].length;
|
|
49
|
+
if (previousHeading > 0 && level > previousHeading + 1) {
|
|
50
|
+
findings.push(`${file}:${lineNumber}: heading salta de h${previousHeading} para h${level}`);
|
|
51
|
+
}
|
|
52
|
+
previousHeading = level;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (/^#{2,6}\s+Props(?:\s|$)/i.test(line)) {
|
|
56
|
+
findings.push(`${file}:${lineNumber}: nomeie a referência como “Propriedades de Componente”`);
|
|
57
|
+
}
|
|
58
|
+
if (/^\|\s*Prop\s*\|/i.test(line) || /\|\s*Default\s*\|/.test(line)) {
|
|
59
|
+
findings.push(`${file}:${lineNumber}: use “Propriedade” e “Padrão” no cabeçalho da tabela`);
|
|
60
|
+
}
|
|
61
|
+
if (/\b(?:pra|pro|pros|numa)\b/i.test(line)) {
|
|
62
|
+
findings.push(`${file}:${lineNumber}: abreviação coloquial na prosa`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (inFence) findings.push(`${file}: fence de código sem fechamento`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const importedFile of importedFiles) {
|
|
70
|
+
if (!files.includes(importedFile)) {
|
|
71
|
+
findings.push(`${importedFile}: importado pelo registry, mas o arquivo não existe`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (findings.length > 0) {
|
|
76
|
+
console.error(findings.join("\n"));
|
|
77
|
+
console.error(`\n${findings.length} problema(s) estrutural(is) encontrado(s).`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`${files.length} páginas auditadas; nenhuma inconsistência estrutural encontrada.`);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { StrictMode } from 'react'
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
3
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
-
import {
|
|
4
|
+
import { OpusProvider, TooltipProvider } from '@softize/opus/ui/react'
|
|
5
5
|
import { fetchClient } from '@softize/opus/client/fetch'
|
|
6
6
|
import { App } from './App.tsx'
|
|
7
7
|
import './index.css'
|
|
8
8
|
|
|
9
9
|
// O trio de providers do padrão da casa — os hooks (useTriggerAction/useFormAction…)
|
|
10
|
-
// exigem QueryClient +
|
|
10
|
+
// exigem QueryClient + OpusProvider, e qualquer Tooltip da base exige o TooltipProvider.
|
|
11
11
|
const queryClient = new QueryClient()
|
|
12
12
|
// Same-origin (/api). Quando o app ganhar server próprio, aponte o baseUrl pra ele.
|
|
13
13
|
const client = fetchClient({ baseUrl: '' })
|
|
@@ -15,11 +15,11 @@ const client = fetchClient({ baseUrl: '' })
|
|
|
15
15
|
createRoot(document.getElementById('root')!).render(
|
|
16
16
|
<StrictMode>
|
|
17
17
|
<QueryClientProvider client={queryClient}>
|
|
18
|
-
<
|
|
18
|
+
<OpusProvider client={client}>
|
|
19
19
|
<TooltipProvider>
|
|
20
20
|
<App />
|
|
21
21
|
</TooltipProvider>
|
|
22
|
-
</
|
|
22
|
+
</OpusProvider>
|
|
23
23
|
</QueryClientProvider>
|
|
24
24
|
</StrictMode>,
|
|
25
25
|
)
|
|
@@ -40,6 +40,7 @@ export interface ConsoleAuditOptions {
|
|
|
40
40
|
redact?: (value: unknown) => unknown
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Sink de auditoria que imprime cada registro no console, com redação opcional de campos sensíveis. */
|
|
43
44
|
export function consoleAudit(options: ConsoleAuditOptions = {}): AuditSink {
|
|
44
45
|
const {
|
|
45
46
|
format = defaultFormat(),
|
|
@@ -40,6 +40,7 @@ export interface BetterAuthDriverOptions {
|
|
|
40
40
|
|
|
41
41
|
const denyAll: CanFn = () => false
|
|
42
42
|
|
|
43
|
+
/** AuthAdapter que valida a sessão no IdP better-auth (`/api/auth/get-session`) com o cookie da request. */
|
|
43
44
|
export function betterAuthSession(options: BetterAuthDriverOptions): AuthAdapter {
|
|
44
45
|
const { baseURL, mapUser, mapTenant, can, timeoutMs = 5000, name = 'better-auth' } = options
|
|
45
46
|
const base = baseURL.replace(/\/+$/, '')
|
package/src/auth/drivers/jwt.ts
CHANGED
|
@@ -79,6 +79,7 @@ export interface JwtAuthOptions<P extends JwtPayload = JwtPayload> {
|
|
|
79
79
|
// Adapter factory
|
|
80
80
|
// =============================================================================
|
|
81
81
|
|
|
82
|
+
/** AuthAdapter que verifica um JWT (Bearer) e projeta o payload em `User` + `can`. */
|
|
82
83
|
export function jwtAuth<P extends JwtPayload = JwtPayload>(
|
|
83
84
|
options: JwtAuthOptions<P>,
|
|
84
85
|
): AuthAdapter {
|
|
@@ -31,6 +31,7 @@ export interface MemoryCache extends CacheAdapter {
|
|
|
31
31
|
size(): number
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** CacheAdapter em memória do processo, com TTL por entrada e limite opcional de itens; adequado a dev e testes. */
|
|
34
35
|
export function memoryCache(options: MemoryCacheOptions = {}): MemoryCache {
|
|
35
36
|
const entries = new Map<string, Entry>()
|
|
36
37
|
const maxEntries = options.maxEntries ?? 10_000
|
|
@@ -50,6 +50,7 @@ export interface FetchClientOptions {
|
|
|
50
50
|
// Adapter factory
|
|
51
51
|
// =============================================================================
|
|
52
52
|
|
|
53
|
+
/** ClientAdapter que chama actions via HTTP `fetch`, classificando falhas de transporte como `dependency`. */
|
|
53
54
|
export function fetchClient(options: FetchClientOptions): ClientAdapter {
|
|
54
55
|
const {
|
|
55
56
|
baseUrl,
|
|
@@ -92,7 +93,7 @@ export function fetchClient(options: FetchClientOptions): ClientAdapter {
|
|
|
92
93
|
error({
|
|
93
94
|
code: 'client.network',
|
|
94
95
|
category: 'dependency',
|
|
95
|
-
message:
|
|
96
|
+
message: `Falha de rede ao chamar ${action.name}`,
|
|
96
97
|
cause: String(cause),
|
|
97
98
|
}),
|
|
98
99
|
)
|
package/src/core/actions.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Opus — `defineAction` factory + type guards
|
|
3
3
|
*
|
|
4
4
|
* `defineAction` é identity function tipada que aceita qualquer variante
|
|
5
5
|
* de `ActionDef` e preserva o subtipo exato pra type narrowing posterior.
|
|
6
6
|
*
|
|
7
|
+
* Quando cliente e servidor consomem a mesma action, o caminho canônico é
|
|
8
|
+
* `defineContract` (parte compartilhável) + `bindAction` (handler, loads e o
|
|
9
|
+
* resto server-only) — ver `contracts.ts`. `defineAction` continua válido e
|
|
10
|
+
* suportado para action server-only, em que não há contrato a compartilhar.
|
|
11
|
+
*
|
|
7
12
|
* Validação estrutural acontece no `Runtime.register()`, não aqui. Esta função
|
|
8
13
|
* é puramente sobre **declaração**.
|
|
9
14
|
*
|
package/src/core/audit.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Opus — AuditEmitter
|
|
3
3
|
*
|
|
4
4
|
* Classe que coordena a emissão de `AuditRecord` pros `AuditSink` registrados.
|
|
5
5
|
* Aplica config da action (redact, fields, severity, sink filter) antes de
|
|
@@ -71,11 +71,17 @@ export class AuditEmitter {
|
|
|
71
71
|
if (failures.length === 0) return
|
|
72
72
|
|
|
73
73
|
if (this.mode === 'strict') {
|
|
74
|
+
this.log.error('audit sinks failed (strict mode)', {
|
|
75
|
+
failures: failures.length,
|
|
76
|
+
total: targetSinks.length,
|
|
77
|
+
reasons: failures.map((f) => String(f.reason)),
|
|
78
|
+
})
|
|
74
79
|
throw error({
|
|
75
80
|
code: 'audit.sink.failed',
|
|
76
81
|
category: 'internal',
|
|
77
|
-
message: `${failures.length}
|
|
78
|
-
|
|
82
|
+
message: `${failures.length} de ${targetSinks.length} sinks de auditoria falharam`,
|
|
83
|
+
// Só o número vai à wire; o motivo cru de cada sink fica no log do servidor.
|
|
84
|
+
meta: { failures: failures.length },
|
|
79
85
|
})
|
|
80
86
|
}
|
|
81
87
|
|
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
|
|
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
|
}
|