@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/cli-shared.mjs +131 -0
  6. package/bin/lib/copy.mjs +276 -6
  7. package/bin/lib/db.mjs +16 -74
  8. package/bin/lib/gen-openapi.mjs +3 -3
  9. package/bin/lib/gen-runner.mjs +1 -1
  10. package/bin/lib/gen.mjs +14 -69
  11. package/bin/lib/mcp.mjs +3 -1
  12. package/bin/lib/seed.mjs +5 -62
  13. package/docs/code-style.md +4 -1
  14. package/docs/ownership-vs-shadcn-lock.md +2 -3
  15. package/docs/protocol.md +7 -7
  16. package/docs/releasing.md +8 -2
  17. package/package.json +7 -3
  18. package/registry/instructions/opus.md +3 -3
  19. package/registry/templates/app/package.json +1 -1
  20. package/registry/templates/app/src/App.tsx +11 -6
  21. package/registry/templates/app/src/main.tsx +4 -4
  22. package/src/audit/drivers/console.ts +1 -0
  23. package/src/auth/drivers/better-auth.ts +1 -0
  24. package/src/auth/drivers/jwt.ts +1 -0
  25. package/src/cache/drivers/memory.ts +1 -0
  26. package/src/client/drivers/fetch.ts +2 -1
  27. package/src/core/actions.ts +6 -1
  28. package/src/core/audit.ts +9 -3
  29. package/src/core/contracts.ts +7 -0
  30. package/src/core/domain.ts +1 -1
  31. package/src/core/errors.ts +18 -15
  32. package/src/core/index.ts +4 -2
  33. package/src/core/package-version.ts +26 -0
  34. package/src/core/reactions.ts +1 -1
  35. package/src/core/runtime.ts +33 -23
  36. package/src/core/schedules.ts +1 -1
  37. package/src/core/types.ts +5 -6
  38. package/src/dsl/eval.ts +2 -2
  39. package/src/dsl/kysely.ts +2 -2
  40. package/src/dsl/loads.ts +1 -1
  41. package/src/dsl/parser.ts +5 -5
  42. package/src/events/drivers/mitt.ts +1 -0
  43. package/src/mcp/index.ts +2 -1
  44. package/src/observability/drivers/opentelemetry.ts +1 -0
  45. package/src/queue/drivers/bullmq.ts +3 -3
  46. package/src/scheduler/drivers/node-cron.ts +3 -2
  47. package/src/scheduler/every.ts +7 -7
  48. package/src/schema/openapi.ts +3 -3
  49. package/src/seed/index.ts +29 -0
  50. package/src/server/drivers/fastify.ts +5 -2
  51. package/src/server/drivers/node.ts +9 -6
  52. package/src/server/index.ts +3 -1
  53. package/src/storage/drivers/fs.ts +1 -0
  54. package/src/testing/index.ts +3 -3
  55. package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
  56. package/src/ui/components/patterns/confirm.tsx +2 -31
  57. package/src/ui/components/patterns/content-header.tsx +44 -137
  58. package/src/ui/components/patterns/data-state.tsx +42 -68
  59. package/src/ui/components/patterns/dock.tsx +20 -3
  60. package/src/ui/components/patterns/form.tsx +26 -22
  61. package/src/ui/components/patterns/list.tsx +18 -12
  62. package/src/ui/components/patterns/page-state.tsx +39 -51
  63. package/src/ui/components/patterns/page.tsx +37 -54
  64. package/src/ui/components/patterns/sidebar.tsx +17 -6
  65. package/src/ui/components/patterns/state-surface.tsx +148 -0
  66. package/src/ui/components/patterns/surface-header.tsx +119 -0
  67. package/src/ui/components/patterns/trigger.tsx +21 -25
  68. package/src/ui/components/patterns/view.tsx +29 -22
  69. package/src/ui/components/primitives/alert.tsx +1 -27
  70. package/src/ui/components/primitives/ask.tsx +3 -3
  71. package/src/ui/components/primitives/avatar.tsx +15 -5
  72. package/src/ui/components/primitives/badge.tsx +5 -41
  73. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  74. package/src/ui/components/primitives/button.tsx +39 -30
  75. package/src/ui/components/primitives/calendar.tsx +28 -2
  76. package/src/ui/components/primitives/carousel.tsx +3 -3
  77. package/src/ui/components/primitives/chat.tsx +1 -1
  78. package/src/ui/components/primitives/checkbox.tsx +1 -1
  79. package/src/ui/components/primitives/command.tsx +2 -2
  80. package/src/ui/components/primitives/control.ts +72 -0
  81. package/src/ui/components/primitives/copyable.tsx +1 -1
  82. package/src/ui/components/primitives/dialog.tsx +12 -7
  83. package/src/ui/components/primitives/dot.tsx +1 -25
  84. package/src/ui/components/primitives/drawer.tsx +10 -3
  85. package/src/ui/components/primitives/field.tsx +3 -3
  86. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  87. package/src/ui/components/primitives/input-group.tsx +12 -9
  88. package/src/ui/components/primitives/input-otp.tsx +1 -1
  89. package/src/ui/components/primitives/input.tsx +2 -2
  90. package/src/ui/components/primitives/item.tsx +3 -1
  91. package/src/ui/components/primitives/menu.tsx +1 -7
  92. package/src/ui/components/primitives/pagination.tsx +16 -8
  93. package/src/ui/components/primitives/progress.tsx +32 -3
  94. package/src/ui/components/primitives/radio-group.tsx +1 -1
  95. package/src/ui/components/primitives/resizable.tsx +3 -1
  96. package/src/ui/components/primitives/select.tsx +6 -6
  97. package/src/ui/components/primitives/slider.tsx +5 -1
  98. package/src/ui/components/primitives/sonner.tsx +3 -0
  99. package/src/ui/components/primitives/spinner.tsx +13 -16
  100. package/src/ui/components/primitives/switch.tsx +5 -1
  101. package/src/ui/components/primitives/tabs.tsx +6 -3
  102. package/src/ui/components/primitives/textarea.tsx +1 -1
  103. package/src/ui/components/primitives/toggle.tsx +9 -4
  104. package/src/ui/components/primitives/tooltip.tsx +1 -0
  105. package/src/ui/docs/changelog.tsx +1 -1
  106. package/src/ui/docs/content/action-form.md +36 -3
  107. package/src/ui/docs/content/action-list-dialog.md +2 -2
  108. package/src/ui/docs/content/action-list.md +13 -2
  109. package/src/ui/docs/content/action-trigger.md +12 -5
  110. package/src/ui/docs/content/action-view.md +11 -3
  111. package/src/ui/docs/content/ask.md +11 -0
  112. package/src/ui/docs/content/avatar.md +7 -3
  113. package/src/ui/docs/content/button.md +30 -14
  114. package/src/ui/docs/content/calendar.md +13 -0
  115. package/src/ui/docs/content/card.md +26 -0
  116. package/src/ui/docs/content/chat.md +20 -0
  117. package/src/ui/docs/content/cli.md +71 -19
  118. package/src/ui/docs/content/communication.md +36 -0
  119. package/src/ui/docs/content/composer.md +15 -0
  120. package/src/ui/docs/content/content.md +17 -1
  121. package/src/ui/docs/content/copyable.md +8 -0
  122. package/src/ui/docs/content/data-state.md +17 -13
  123. package/src/ui/docs/content/detail.md +19 -1
  124. package/src/ui/docs/content/dialog.md +1 -4
  125. package/src/ui/docs/content/dictionary-value.md +9 -2
  126. package/src/ui/docs/content/dock.md +8 -0
  127. package/src/ui/docs/content/dot.md +8 -0
  128. package/src/ui/docs/content/empty.md +2 -2
  129. package/src/ui/docs/content/getting-started.md +2 -2
  130. package/src/ui/docs/content/icon-picker.md +11 -0
  131. package/src/ui/docs/content/input.md +1 -1
  132. package/src/ui/docs/content/item.md +1 -1
  133. package/src/ui/docs/content/label.md +7 -0
  134. package/src/ui/docs/content/menu.md +6 -0
  135. package/src/ui/docs/content/metric-card.md +13 -0
  136. package/src/ui/docs/content/page.md +20 -4
  137. package/src/ui/docs/content/pagination.md +11 -9
  138. package/src/ui/docs/content/popover.md +6 -0
  139. package/src/ui/docs/content/progress.md +8 -11
  140. package/src/ui/docs/content/select.md +5 -5
  141. package/src/ui/docs/content/semantic-context.md +5 -4
  142. package/src/ui/docs/content/sidebar.md +13 -47
  143. package/src/ui/docs/content/skeleton.md +6 -0
  144. package/src/ui/docs/content/spinner.md +9 -6
  145. package/src/ui/docs/content/split.md +21 -0
  146. package/src/ui/docs/content/switch.md +1 -1
  147. package/src/ui/docs/content/tabs.md +1 -1
  148. package/src/ui/docs/content/textarea.md +7 -0
  149. package/src/ui/docs/content/toggle.md +1 -1
  150. package/src/ui/docs/content/tokens.md +4 -4
  151. package/src/ui/docs/content/truncate.md +8 -0
  152. package/src/ui/docs/content/ui.md +14 -0
  153. package/src/ui/docs/doc-client.tsx +5 -5
  154. package/src/ui/docs/doc.tsx +26 -14
  155. package/src/ui/docs/registry.tsx +5 -5
  156. package/src/ui/docs/standalone.tsx +2 -2
  157. package/src/ui/drivers/react.tsx +12 -12
  158. package/src/ui/lib/action-errors.ts +45 -0
  159. package/src/ui/lib/zod-pt-br.ts +31 -4
  160. package/src/ui/meta.ts +8 -8
  161. package/src/ui/react.tsx +10 -16
  162. package/src/ui/theme.css +10 -8
  163. package/src/vite/design.ts +6 -18
  164. package/src/ui/components/patterns/shell-nav.tsx +0 -147
@@ -82,15 +82,15 @@ function buildResponses(action) {
82
82
  const successStatus = String(action.successStatus ?? 200)
83
83
  return {
84
84
  [successStatus]: {
85
- description: 'Success',
85
+ description: 'Sucesso',
86
86
  content: {
87
87
  'application/json': {
88
88
  schema: wrapEnvelope(action.output, action.kind),
89
89
  },
90
90
  },
91
91
  },
92
- '4XX': errorResponse('Client error', action.errors),
93
- '5XX': errorResponse('Server error'),
92
+ '4XX': errorResponse('Erro do cliente', action.errors),
93
+ '5XX': errorResponse('Erro do servidor'),
94
94
  }
95
95
  }
96
96
 
@@ -75,7 +75,7 @@ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
75
75
  async function main() {
76
76
  const configPath = process.argv[2]
77
77
  if (typeof configPath !== 'string' || configPath.length === 0) {
78
- return emitError('Missing config path argv[2]')
78
+ return emitError('Caminho do config ausente (argv[2])')
79
79
  }
80
80
 
81
81
  const configAbs = path.resolve(configPath)
package/bin/lib/gen.mjs CHANGED
@@ -16,17 +16,13 @@
16
16
  * - config: ./opus.config.ts (no cwd)
17
17
  * - output: o que o config disser, ou ./.gen
18
18
  *
19
- * Flag `--force`: sobrescreve arquivos existentes (default sobrescreve
20
- * silenciosamente — opus gen é idempotente e o output mora num dir
21
- * dedicado, então não há valor real em bloquear sem --force. Mantemos a
22
- * flag pra paridade com `add`).
19
+ * Flag `--force`: aceita por compatibilidade, sem efeito — o gen sempre
20
+ * sobrescreve (é idempotente e o output mora num dir dedicado, então não
21
+ * há valor real em bloquear a escrita).
23
22
  */
24
23
 
25
- import { promises as fs } from 'node:fs'
26
24
  import path from 'node:path'
27
25
  import { fileURLToPath } from 'node:url'
28
- import { execFile } from 'node:child_process'
29
- import { promisify } from 'node:util'
30
26
  import {
31
27
  canonicalProjectDirectory,
32
28
  ensureProjectDirectory,
@@ -42,46 +38,25 @@ import { buildOpenAPI } from './gen-openapi.mjs'
42
38
  import { buildDocs } from './gen-docs.mjs'
43
39
  import { buildClientStubs } from './gen-stubs.mjs'
44
40
  import { buildDictStubs } from './gen-dicts.mjs'
45
-
46
- const execFileAsync = promisify(execFile)
41
+ import { lastJsonLine, log, resolveConfig, spawnTsx } from './cli-shared.mjs'
47
42
 
48
43
  const __filename = fileURLToPath(import.meta.url)
49
44
  const __dirname = path.dirname(__filename)
50
- const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
51
45
  const RUNNER_PATH = path.join(__dirname, 'gen-runner.mjs')
52
46
 
53
- const COLORS = {
54
- info: '\x1b[36m',
55
- success: '\x1b[32m',
56
- error: '\x1b[31m',
57
- warn: '\x1b[33m',
58
- dim: '\x1b[2m',
59
- }
60
- const RESET = '\x1b[0m'
61
-
62
- function log(level, msg) {
63
- console.log(`${COLORS[level] ?? ''}${msg}${RESET}`)
64
- }
65
-
66
47
  // =============================================================================
67
48
  // Comando
68
49
  // =============================================================================
69
50
 
70
51
  export async function cmdGen(flags) {
71
- const cwd = canonicalProjectDirectory(process.cwd())
72
- const configRel = flags.config ?? 'opus.config.ts'
73
- const config = safeProjectPath(cwd, configRel)
74
- const configPath = config.path
75
-
76
- if (!config.exists) {
77
- log('error', `opus.config.ts não encontrado em ${configPath}`)
78
- log(
79
- 'dim',
80
- ' Cria um arquivo na raiz do projeto exportando `{ domains: [...] }` como default,',
81
- )
82
- log('dim', ' ou passa o path via --config <path>.')
52
+ let resolved
53
+ try {
54
+ resolved = resolveConfig(flags)
55
+ } catch (error) {
56
+ log('error', error instanceof Error ? error.message : String(error))
83
57
  process.exit(1)
84
58
  }
59
+ const { cwd, configPath } = resolved
85
60
 
86
61
  log('info', `→ Carregando ${path.relative(cwd, configPath)}...`)
87
62
 
@@ -190,22 +165,12 @@ function auditDocs(manifest, cwd) {
190
165
  // =============================================================================
191
166
 
192
167
  async function loadConfig(configPath) {
193
- // tsx mora em <package>/node_modules/.bin/tsx usamos absoluto pra
194
- // evitar depender de PATH do consumer.
195
- const tsxBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
196
- const hasLocalTsx = await fileExists(tsxBin)
197
-
198
- // Args do runner: <runner.mjs> <configPath>. tsx aceita .mjs também
199
- // (passthrough) e nos permite resolver TS na cadeia de imports.
200
- const args = [RUNNER_PATH, configPath]
201
- const cmd = hasLocalTsx ? tsxBin : 'tsx'
202
-
168
+ // tsx aceita .mjs também (passthrough) e nos permite resolver TS na cadeia
169
+ // de imports do config.
203
170
  try {
204
171
  // Buffer grande pra acomodar manifests gordos.
205
- const { stdout, stderr } = await execFileAsync(cmd, args, {
206
- encoding: 'utf8',
172
+ const { stdout, stderr } = await spawnTsx(RUNNER_PATH, [configPath], {
207
173
  maxBuffer: 64 * 1024 * 1024,
208
- env: { ...process.env },
209
174
  })
210
175
 
211
176
  // Runner emite a linha JSON na ÚLTIMA linha de stdout. Qualquer log do
@@ -236,30 +201,10 @@ async function loadConfig(configPath) {
236
201
  }
237
202
  }
238
203
 
239
- function lastJsonLine(stdout) {
240
- const lines = stdout.split('\n').filter((l) => l.trim().length > 0)
241
- for (let i = lines.length - 1; i >= 0; i--) {
242
- const trimmed = lines[i].trim()
243
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
244
- return trimmed
245
- }
246
- }
247
- return null
248
- }
249
-
250
204
  // =============================================================================
251
205
  // FS helpers
252
206
  // =============================================================================
253
207
 
254
- async function fileExists(p) {
255
- try {
256
- await fs.access(p)
257
- return true
258
- } catch {
259
- return false
260
- }
261
- }
262
-
263
208
  function ensureProjectTree(root, requested) {
264
209
  let cursor = '.'
265
210
  for (const segment of requested.split(path.sep).filter((item) => item !== '' && item !== '.')) {
@@ -332,7 +277,7 @@ Lê opus.config.ts do consumer e gera:
332
277
  Flags:
333
278
  --config <path> Caminho interno ao projeto para opus.config.ts. Default: ./opus.config.ts
334
279
  --output <path> Pasta interna ao projeto. Default: do config (ou ./.gen)
335
- --force, -f Sobrescreve existente (atualmente sempre sobrescreve)
280
+ --force, -f Aceita por compatibilidade; sem efeito, o gen sempre sobrescreve a saída
336
281
 
337
282
  Exemplos:
338
283
  npx @softize/opus gen
package/bin/lib/mcp.mjs CHANGED
@@ -11,6 +11,7 @@
11
11
  * opus_get_component — detalhe de um componente pelo nome
12
12
  */
13
13
 
14
+ import { createRequire } from 'node:module'
14
15
  import path from 'node:path'
15
16
  import { z } from 'zod'
16
17
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
@@ -23,10 +24,11 @@ const asText = (data) => ({
23
24
  content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }],
24
25
  })
25
26
  const dirOf = (d) => path.resolve(process.cwd(), d ?? '.')
27
+ const { version: OPUS_VERSION } = createRequire(import.meta.url)('../../package.json')
26
28
 
27
29
  /** Monta o McpServer com as tools da base. Exportado pra testar (transport in-memory). */
28
30
  export function buildServer() {
29
- const server = new McpServer({ name: 'opus', version: '0.0.0' })
31
+ const server = new McpServer({ name: 'opus', version: OPUS_VERSION })
30
32
 
31
33
  server.registerTool(
32
34
  'opus_introspect',
package/bin/lib/seed.mjs CHANGED
@@ -1,30 +1,13 @@
1
1
  /** Grupo `opus seed`: descoberta, gate e execução segura dos seeds do projeto. */
2
2
 
3
- import { execFile } from 'node:child_process'
4
- import { promises as fs } from 'node:fs'
5
3
  import path from 'node:path'
6
4
  import { fileURLToPath } from 'node:url'
7
- import { promisify } from 'node:util'
8
5
 
9
- import { canonicalProjectDirectory, readProjectFile, safeProjectPath } from '@softize/base/project-path'
6
+ import { readProjectFile } from '@softize/base/project-path'
10
7
 
11
- const execFileAsync = promisify(execFile)
12
- const __filename = fileURLToPath(import.meta.url)
13
- const __dirname = path.dirname(__filename)
14
- const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
8
+ import { log, resolveConfig, runJsonRunner } from './cli-shared.mjs'
15
9
 
16
- const COLORS = {
17
- info: '\x1b[36m',
18
- success: '\x1b[32m',
19
- error: '\x1b[31m',
20
- warn: '\x1b[33m',
21
- dim: '\x1b[2m',
22
- }
23
- const RESET = '\x1b[0m'
24
-
25
- function log(level, message) {
26
- console.log(`${COLORS[level] ?? ''}${message}${RESET}`)
27
- }
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
28
11
 
29
12
  export function findAdHocSeedScripts(scripts = {}) {
30
13
  return Object.entries(scripts).flatMap(([name, command]) => {
@@ -116,14 +99,6 @@ function fail(flags, message) {
116
99
  process.exitCode = 1
117
100
  }
118
101
 
119
- function resolveConfig(flags) {
120
- const cwd = canonicalProjectDirectory(process.cwd())
121
- const configRel = flags.config ?? 'opus.config.ts'
122
- const config = safeProjectPath(cwd, configRel)
123
- if (!config.exists) throw new Error(`opus.config.ts não encontrado em ${config.path}`)
124
- return { cwd, configPath: config.path }
125
- }
126
-
127
102
  function readSeedScriptFindings(cwd) {
128
103
  const packageFile = readProjectFile(cwd, 'package.json', { allowMissing: true })
129
104
  if (!packageFile.exists) return []
@@ -140,22 +115,8 @@ function readSeedScriptFindings(cwd) {
140
115
  }))
141
116
  }
142
117
 
143
- async function runRunner(configPath, extraArgs) {
144
- const tsxBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
145
- const command = (await fileExists(tsxBin)) ? tsxBin : 'tsx'
146
- const runnerPath = path.join(__dirname, 'seed-runner.mjs')
147
- try {
148
- const { stdout } = await execFileAsync(command, [runnerPath, configPath, ...extraArgs], {
149
- encoding: 'utf8',
150
- maxBuffer: 16 * 1024 * 1024,
151
- env: { ...process.env },
152
- })
153
- const line = lastJsonLine(stdout)
154
- return line === null ? { ok: false, error: 'runner não emitiu JSON' } : JSON.parse(line)
155
- } catch (error) {
156
- const detail = typeof error.stderr === 'string' && error.stderr.trim() ? error.stderr.trim() : error.message
157
- return { ok: false, error: detail }
158
- }
118
+ function runRunner(configPath, extraArgs) {
119
+ return runJsonRunner(path.join(__dirname, 'seed-runner.mjs'), [configPath, ...extraArgs])
159
120
  }
160
121
 
161
122
  function printList(seeds) {
@@ -188,24 +149,6 @@ function printDiagnostics(diagnostics) {
188
149
  for (const diagnostic of diagnostics) log('warn', `[${diagnostic.code}] ${diagnostic.message}`)
189
150
  }
190
151
 
191
- function lastJsonLine(stdout) {
192
- const lines = stdout.split('\n').filter((line) => line.trim())
193
- for (let index = lines.length - 1; index >= 0; index--) {
194
- const line = lines[index].trim()
195
- if (line.startsWith('{') && line.endsWith('}')) return line
196
- }
197
- return null
198
- }
199
-
200
- async function fileExists(file) {
201
- try {
202
- await fs.access(file)
203
- return true
204
- } catch {
205
- return false
206
- }
207
- }
208
-
209
152
  export function helpSeed() {
210
153
  console.log(`
211
154
  @softize/opus seed — datasets estruturados do projeto
@@ -86,13 +86,16 @@ Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada r
86
86
  | `messages.success`, `messages.error`, `messages.confirmation` | `success`, `error`, `message` |
87
87
  | `confirm.message` | `dialog-body` |
88
88
  | `fields.*.label`, `filters.*.label` | `label` |
89
- | `placeholder`, `hint`, `help` | `placeholder`, `helper-text` |
89
+ | `placeholder`, `help` | `placeholder`, `helper-text` |
90
90
  | opções estáticas | `menu-item` |
91
91
  | `columns[].label`, `periods[].label` | `heading`, `tab` |
92
92
  | `Select.emptyText`, `Select.searchPlaceholder` | `empty-state`, `placeholder` |
93
93
  | `Select.options[].hint/triggerLabel/group` | `label`, `label`, `heading` |
94
94
  | `ActionTrigger.confirm.*` | papel correspondente do diálogo |
95
95
  | `t.dict` — `label`, `description`, `doc` das entradas e `doc` do dicionário | `label`, `description` |
96
+ | `DataState`/`PageState`/`ActionList`/`ActionListDialog` — `emptyMessage`, `errorMessage`, `retryLabel`; `PageState`/`ActionListDialog` — `title`, `description`; `ActionView.emptyMessage` | `empty-state`, `error`, `button`, `title`, `description` |
97
+ | `dialog.alert/confirm/prompt/choose()` — `title`, `description`, `body`, `action`, `cancel`, `placeholder`, `actions[].label` | papel correspondente do diálogo |
98
+ | `TooltipContent` (children), `LabelHelp.help` | `label`, `helper-text` |
96
99
 
97
100
  ## Cobertura e significado do gate verde
98
101
 
@@ -73,9 +73,8 @@ upstream com quem se tem que ficar idêntico atrita com ter opinião.
73
73
  3. **Re-sync por diff sob demanda, com IA.** Em vez de "re-porta e o hash diz o que
74
74
  mudou", o fluxo é um **merge de 3 vias**: base (o `upstreamHash` gravado) → shadcn
75
75
  atual → nosso customizado. O Claude lê os dois diffs e reconcilia preservando o delta
76
- da casa. Vale uma skill/comando (`opus resync <componente>`) pra ser um gesto de uma
77
- linha, com teste + revisão como rede de segurança (o hash determinístico sai, o
78
- julgamento entra).
76
+ da casa. Hoje é um fluxo conduzido com IA, com teste + revisão como rede de segurança
77
+ (o hash determinístico sai, o julgamento entra); não existe comando `opus resync`.
79
78
 
80
79
  ## Tradeoffs honestos
81
80
 
package/docs/protocol.md CHANGED
@@ -424,21 +424,21 @@ handler: async (ctx, input) => {
424
424
  if (!deal) throw error({
425
425
  code: 'deal.archive.notFound',
426
426
  category: 'not_found',
427
- message: `Deal ${input.dealId} not found`,
427
+ message: `Negócio ${input.dealId} não encontrado`,
428
428
  i18nKey: 'deal.error.notFound',
429
429
  i18nParams: { id: input.dealId },
430
430
  })
431
431
  if (deal.archivedAt) throw error({
432
432
  code: 'deal.archive.alreadyArchived',
433
433
  category: 'conflict',
434
- message: 'Deal already archived',
434
+ message: 'Negócio arquivado',
435
435
  retriable: false,
436
436
  })
437
437
  // ...
438
438
  }
439
439
  ```
440
440
 
441
- Erros não-categorizados (ex: `throw new Error(...)` cru, ou exceção do driver do DB) são **normalizados** pelo runtime para `{ category: 'internal', code: 'internal.unhandled', severity: 'error', retriable: false, cause: <original> }`.
441
+ Erros não-categorizados (ex: `throw new Error(...)` cru, ou exceção do driver do DB) são **normalizados** pelo runtime para `{ category: 'internal', code: 'internal.unhandled', severity: 'error', retriable: false, message: 'Não foi possível concluir a operação. Tente novamente.', cause: <original> }`. A `message` é fixa: o texto da exceção original nunca chega à pessoa — fica no logger do runtime e em `cause` (só a mensagem; stack não vaza).
442
442
 
443
443
  ### Validation: caso especial
444
444
 
@@ -448,12 +448,12 @@ Erros de schema (input que não satisfaz Zod/ArkType) são **automaticamente** t
448
448
  {
449
449
  code: 'validation.invalid_input',
450
450
  category: 'validation',
451
- message: 'Input validation failed',
451
+ message: 'Dados de entrada inválidos',
452
452
  severity: 'warning',
453
453
  retriable: false,
454
454
  issues: [
455
- { path: 'dealId', code: 'required', message: 'dealId is required' },
456
- { path: 'priority', code: 'out_of_range', message: 'priority must be 1..5' },
455
+ { path: 'dealId', code: 'required', message: 'dealId é obrigatório' },
456
+ { path: 'priority', code: 'out_of_range', message: 'priority precisa estar entre 1 e 5' },
457
457
  ],
458
458
  }
459
459
  ```
@@ -540,7 +540,7 @@ authorize: async (ctx, input, loaded) => {
540
540
  if (loaded.deal.locked) return error({ // específico
541
541
  code: 'deal.archive.locked',
542
542
  category: 'authorization',
543
- message: 'Locked deals cannot be archived',
543
+ message: 'Negócios bloqueados não podem ser arquivados',
544
544
  })
545
545
  return ctx.can('deal:archive', loaded.deal) // delega
546
546
  }
package/docs/releasing.md CHANGED
@@ -53,17 +53,21 @@ pra testar publicação seria a guarda atrapalhando quem está experimentando.
53
53
 
54
54
  **Gate de qualidade**: depois do bump e da materialização, mas antes de publicar, roda
55
55
  `pnpm typecheck` + `pnpm test` + `pnpm copy:check` + `base copy check` e **aborta a release
56
- se qualquer um falhar**. O Opus declara `@softize/base ^2.1.0` em `dependencies`, pois usa
56
+ se qualquer um falhar**. O Opus declara `@softize/base ^2.2.1` em `dependencies`, pois usa
57
57
  suas APIs públicas de filesystem em runtime, e materializa os artefatos Base no próprio repo;
58
58
  a release não baixa uma política ad hoc. Como o
59
59
  Opus **ship source** (`.ts`, sem build), essa é a última barreira antes do tarball — sem ela,
60
60
  um `tsc` vermelho, inventário desatualizado ou violação da política vaza pro cliente. Emergência (evite):
61
61
  `pnpm release --skip-checks`.
62
62
 
63
+ O gate roda `pnpm test`, não `pnpm test:cov`: o threshold de 100% de cobertura em
64
+ `vitest.config.ts` é uma meta aspiracional e **não** faz parte do gate de release. Uma release
65
+ não é bloqueada por cobertura abaixo de 100%; a cobertura é acompanhada com `pnpm test:cov`.
66
+
63
67
  **Smoke do esqueleto**: depois do bump e antes do publish, o `release.sh` gera um app com
64
68
  `opus create`, instala o **tarball exato** que vai ser publicado e roda os gates dele
65
69
  (typecheck · test · `opus check` · `opus copy --check` · `base copy check` · manifest ·
66
- build). O template exige `@softize/base ^2.1.0`; publique a Base compatível antes do Opus.
70
+ build). O template exige `@softize/base ^2.2.1`; publique a Base compatível antes do Opus.
67
71
  O `minimumReleaseAgeExclude` do template inclui os dois pacotes, e o smoke executa os
68
72
  fragmentos de pre-push para provar que o layout pnpm instalado resolve ambos os CLIs.
69
73
  É o que pega o que typecheck+test não
@@ -93,6 +97,8 @@ sem `link:`/`file:`, que podem mascarar exports e peers.
93
97
  ```bash
94
98
  # terminal 1 — uma vez por máquina/sessão
95
99
  pnpm registry:up # Verdaccio em 127.0.0.1:6873
100
+ # `registry:up` lê `~/.config/verdaccio/config.yaml`, que não é versionado: cada estação
101
+ # mantém o seu, com `listen: 127.0.0.1:6873` (a porta que `release.sh --local` espera).
96
102
 
97
103
  # terminal 2 — no Opus
98
104
  pnpm version 8.7.0-rc.0 --no-git-tag-version # escolha uma versão ainda não publicada
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "13.0.0",
3
+ "version": "14.0.0",
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",
@@ -202,6 +202,7 @@
202
202
  "docs",
203
203
  "README.md",
204
204
  "CHANGELOG.md",
205
+ "PROMOTED.md",
205
206
  "LICENSE"
206
207
  ],
207
208
  "publishConfig": {
@@ -259,7 +260,8 @@
259
260
  "node-cron": "^3.0.0",
260
261
  "pg": "^8.0.0",
261
262
  "pino": "^9.0.0",
262
- "react": "^18.0.0 || ^19.0.0",
263
+ "react": "^19.0.0",
264
+ "react-dom": "^19.0.0",
263
265
  "react-hook-form": "^7.0.0",
264
266
  "vite": "^5.0.0 || ^6.0.0",
265
267
  "zod": "^3.24.0"
@@ -301,6 +303,9 @@
301
303
  "react": {
302
304
  "optional": true
303
305
  },
306
+ "react-dom": {
307
+ "optional": true
308
+ },
304
309
  "vite": {
305
310
  "optional": true
306
311
  },
@@ -328,7 +333,6 @@
328
333
  "@tanstack/react-query": "^5.62.0",
329
334
  "@testing-library/dom": "^10.4.0",
330
335
  "@testing-library/react": "^16.1.0",
331
- "@types/better-sqlite3": "^7.6.0",
332
336
  "@types/jsonwebtoken": "^9.0.0",
333
337
  "@types/node": "^22.0.0",
334
338
  "@types/node-cron": "^3.0.0",
@@ -17,9 +17,9 @@ artefatos de agentes fica em `base.json`; cada app Opus mantém seu marcador `op
17
17
  - Não duplicar schemas, tipos de transporte, validação ou fetch que o contrato já fornece.
18
18
  - Em UI semântica, declarar primeiro `context` (`neutral`, `primary`, `info`, `success`, `warning`
19
19
  ou `danger`) e usar `variant` somente para o tratamento visual (`solid`, `subtle`, `outline`,
20
- `ghost` ou `link`). Dicionários de status e estágio declaram `context`; `tone` e variantes
21
- semânticas antigas são apenas compatibilidade de migração. `destructive` permanece uma
22
- propriedade comportamental de actions e se projeta visualmente como `danger`.
20
+ `ghost` ou `link`). Dicionários de status e estágio declaram `context`; `tone` é apenas
21
+ compatibilidade de migração e as variantes semânticas antigas não existem mais. `destructive`
22
+ permanece uma propriedade comportamental de actions e se projeta visualmente como `danger`.
23
23
  - Declarar datasets persistentes com `defineSeed` + `bindSeed`, registrá-los em `opus.config.ts`
24
24
  e operá-los por `opus seed`; não criar comandos de seed paralelos nem reset implícito.
25
25
  - Regenerar o inventário com `opus copy` quando mudar copy em contrato ou componente Opus
@@ -30,7 +30,7 @@
30
30
  "zod": "^3.24.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@softize/base": "^2.1.0",
33
+ "@softize/base": "^2.2.1",
34
34
  "@tailwindcss/vite": "^4.1.0",
35
35
  "@types/node": "^22.0.0",
36
36
  "@types/react": "^19.0.0",
@@ -1,5 +1,5 @@
1
1
  import type { z } from 'zod'
2
- import { Badge, Card, useListAction } from '@softize/opus/ui/react'
2
+ import { Badge, Card, DataState, useListAction } from '@softize/opus/ui/react'
3
3
  import { Task, taskList } from './domains/tasks/actions/list.ts'
4
4
 
5
5
  type TaskItem = z.infer<typeof Task>
@@ -10,7 +10,7 @@ export function App(): React.ReactElement {
10
10
  // O front consome o CONTRATO via /api — o plugin opusDesign (vite.config.ts)
11
11
  // serve o backend no próprio dev server: dev normal executa o handler real;
12
12
  // `pnpm dev:design` responde com o mockHandler, isolado de qualquer backend.
13
- const { items, isLoading } = useListAction<TaskItem>(taskList)
13
+ const { items, isLoading, error, refetch } = useListAction<TaskItem>(taskList)
14
14
 
15
15
  return (
16
16
  <main className="flex min-h-full items-center justify-center bg-background p-6">
@@ -19,9 +19,14 @@ export function App(): React.ReactElement {
19
19
  <p className="mb-4 text-sm text-muted-foreground">
20
20
  Esqueleto criado pelo opus create. A spec vive nas declarações do domínio.
21
21
  </p>
22
- {isLoading ? (
23
- <p className="text-sm text-muted-foreground">Carregando…</p>
24
- ) : (
22
+ {/* Carregando, erro e vazio saem do DataState; a lista só cuida dos itens. */}
23
+ <DataState
24
+ loading={isLoading}
25
+ error={error ?? null}
26
+ empty={items.length === 0}
27
+ emptyMessage="Nenhuma tarefa ainda."
28
+ onRetry={() => refetch()}
29
+ >
25
30
  <ul className="space-y-2">
26
31
  {items.map((t) => (
27
32
  <li key={t.id} className="flex items-center justify-between gap-2 text-sm">
@@ -30,7 +35,7 @@ export function App(): React.ReactElement {
30
35
  </li>
31
36
  ))}
32
37
  </ul>
33
- )}
38
+ </DataState>
34
39
  </Card>
35
40
  </main>
36
41
  )
@@ -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 { TbdlibProvider, TooltipProvider } from '@softize/opus/ui/react'
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 + Tbdlib, e qualquer Tooltip da base exige o TooltipProvider.
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
- <TbdlibProvider client={client}>
18
+ <OpusProvider client={client}>
19
19
  <TooltipProvider>
20
20
  <App />
21
21
  </TooltipProvider>
22
- </TbdlibProvider>
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(/\/+$/, '')
@@ -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: 'Network request failed',
96
+ message: `Falha de rede ao chamar ${action.name}`,
96
97
  cause: String(cause),
97
98
  }),
98
99
  )
@@ -1,9 +1,14 @@
1
1
  /**
2
- * tbdlib — `defineAction` factory + type guards
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
- * tbdlib — AuditEmitter
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}/${targetSinks.length} audit sinks failed`,
78
- meta: { failures: failures.map((f) => String(f.reason)) },
82
+ message: `${failures.length} de ${targetSinks.length} sinks de auditoria falharam`,
83
+ // 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