@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.
Files changed (213) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/check.mjs +2 -7
  6. package/bin/lib/cli-shared.mjs +131 -0
  7. package/bin/lib/copy.mjs +1 -5
  8. package/bin/lib/db.mjs +16 -74
  9. package/bin/lib/gen-openapi.mjs +3 -3
  10. package/bin/lib/gen-runner.mjs +1 -1
  11. package/bin/lib/gen.mjs +14 -69
  12. package/bin/lib/mcp.mjs +3 -1
  13. package/bin/lib/seed.mjs +5 -62
  14. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
  15. package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
  16. package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
  17. package/docs/ownership-vs-shadcn-lock.md +2 -3
  18. package/docs/protocol.md +7 -7
  19. package/docs/radius-scale.md +1 -1
  20. package/docs/releasing.md +8 -2
  21. package/package.json +7 -3
  22. package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
  23. package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
  24. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
  25. package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
  26. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
  27. package/registry/templates/app/package.json +1 -1
  28. package/registry/templates/app/src/main.tsx +4 -4
  29. package/src/audit/drivers/console.ts +1 -0
  30. package/src/auth/drivers/better-auth.ts +1 -0
  31. package/src/auth/drivers/jwt.ts +1 -0
  32. package/src/cache/drivers/memory.ts +1 -0
  33. package/src/client/drivers/fetch.ts +2 -1
  34. package/src/core/actions.ts +6 -1
  35. package/src/core/audit.ts +9 -3
  36. package/src/core/contracts.ts +7 -0
  37. package/src/core/domain.ts +1 -1
  38. package/src/core/errors.ts +18 -15
  39. package/src/core/index.ts +4 -2
  40. package/src/core/package-version.ts +26 -0
  41. package/src/core/reactions.ts +1 -1
  42. package/src/core/runtime.ts +33 -23
  43. package/src/core/schedules.ts +1 -1
  44. package/src/core/types.ts +2 -2
  45. package/src/dsl/eval.ts +2 -2
  46. package/src/dsl/kysely.ts +2 -2
  47. package/src/dsl/loads.ts +1 -1
  48. package/src/dsl/parser.ts +5 -5
  49. package/src/events/drivers/mitt.ts +1 -0
  50. package/src/mcp/index.ts +2 -1
  51. package/src/observability/drivers/opentelemetry.ts +1 -0
  52. package/src/queue/drivers/bullmq.ts +3 -3
  53. package/src/scheduler/drivers/node-cron.ts +3 -2
  54. package/src/scheduler/every.ts +7 -7
  55. package/src/schema/openapi.ts +3 -3
  56. package/src/seed/index.ts +29 -0
  57. package/src/server/drivers/fastify.ts +5 -2
  58. package/src/server/drivers/node.ts +9 -6
  59. package/src/server/index.ts +3 -1
  60. package/src/storage/drivers/fs.ts +1 -0
  61. package/src/testing/index.ts +3 -3
  62. package/src/ui/components/patterns/confirm.tsx +142 -42
  63. package/src/ui/components/patterns/content-header.tsx +7 -1
  64. package/src/ui/components/patterns/data-state.tsx +1 -1
  65. package/src/ui/components/patterns/dock.tsx +20 -3
  66. package/src/ui/components/patterns/form.tsx +12 -8
  67. package/src/ui/components/patterns/list.tsx +36 -41
  68. package/src/ui/components/patterns/page-state.tsx +2 -2
  69. package/src/ui/components/patterns/page.tsx +19 -1
  70. package/src/ui/components/patterns/shell-nav.tsx +10 -3
  71. package/src/ui/components/patterns/sidebar.tsx +43 -32
  72. package/src/ui/components/patterns/trigger.tsx +39 -38
  73. package/src/ui/components/patterns/view.tsx +26 -17
  74. package/src/ui/components/primitives/alert.tsx +14 -8
  75. package/src/ui/components/primitives/ask.tsx +3 -3
  76. package/src/ui/components/primitives/badge.tsx +11 -6
  77. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  78. package/src/ui/components/primitives/button.tsx +16 -3
  79. package/src/ui/components/primitives/calendar.tsx +28 -2
  80. package/src/ui/components/primitives/carousel.tsx +3 -3
  81. package/src/ui/components/primitives/chat.tsx +1 -1
  82. package/src/ui/components/primitives/checkbox.tsx +1 -1
  83. package/src/ui/components/primitives/command.tsx +2 -2
  84. package/src/ui/components/primitives/control.ts +12 -0
  85. package/src/ui/components/primitives/copyable.tsx +1 -1
  86. package/src/ui/components/primitives/dialog.tsx +202 -40
  87. package/src/ui/components/primitives/dot.tsx +5 -0
  88. package/src/ui/components/primitives/drawer.tsx +18 -8
  89. package/src/ui/components/primitives/empty.tsx +3 -3
  90. package/src/ui/components/primitives/field.tsx +3 -3
  91. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  92. package/src/ui/components/primitives/input-group.tsx +1 -1
  93. package/src/ui/components/primitives/input-otp.tsx +1 -1
  94. package/src/ui/components/primitives/input.tsx +2 -2
  95. package/src/ui/components/primitives/item.tsx +3 -3
  96. package/src/ui/components/primitives/progress.tsx +32 -3
  97. package/src/ui/components/primitives/radio-group.tsx +1 -1
  98. package/src/ui/components/primitives/resizable.tsx +3 -1
  99. package/src/ui/components/primitives/select.tsx +5 -5
  100. package/src/ui/components/primitives/slider.tsx +5 -1
  101. package/src/ui/components/primitives/sonner.tsx +190 -8
  102. package/src/ui/components/primitives/switch.tsx +1 -0
  103. package/src/ui/components/primitives/tabs.tsx +1 -0
  104. package/src/ui/components/primitives/textarea.tsx +1 -1
  105. package/src/ui/components/primitives/toggle.tsx +1 -1
  106. package/src/ui/components/primitives/tooltip.tsx +1 -0
  107. package/src/ui/docs/DocBrowser.tsx +102 -23
  108. package/src/ui/docs/changelog.tsx +1 -1
  109. package/src/ui/docs/content/accordion.md +22 -16
  110. package/src/ui/docs/content/action-form-card.md +8 -8
  111. package/src/ui/docs/content/action-form-dialog.md +9 -9
  112. package/src/ui/docs/content/action-form.md +37 -36
  113. package/src/ui/docs/content/action-list-dialog.md +11 -6
  114. package/src/ui/docs/content/action-list.md +73 -39
  115. package/src/ui/docs/content/action-trigger.md +29 -15
  116. package/src/ui/docs/content/action-view.md +17 -10
  117. package/src/ui/docs/content/actions.md +9 -9
  118. package/src/ui/docs/content/ai.md +3 -3
  119. package/src/ui/docs/content/alert.md +14 -12
  120. package/src/ui/docs/content/ask.md +11 -0
  121. package/src/ui/docs/content/aspect-ratio.md +4 -4
  122. package/src/ui/docs/content/audit.md +2 -2
  123. package/src/ui/docs/content/auth.md +3 -3
  124. package/src/ui/docs/content/avatar.md +34 -14
  125. package/src/ui/docs/content/badge.md +3 -3
  126. package/src/ui/docs/content/breadcrumb.md +13 -8
  127. package/src/ui/docs/content/button.md +81 -6
  128. package/src/ui/docs/content/calendar.md +18 -5
  129. package/src/ui/docs/content/card.md +27 -1
  130. package/src/ui/docs/content/carousel.md +16 -11
  131. package/src/ui/docs/content/chat.md +23 -3
  132. package/src/ui/docs/content/checkbox.md +7 -7
  133. package/src/ui/docs/content/cli.md +74 -22
  134. package/src/ui/docs/content/collapsible.md +8 -8
  135. package/src/ui/docs/content/command.md +16 -8
  136. package/src/ui/docs/content/composer.md +17 -2
  137. package/src/ui/docs/content/content.md +17 -2
  138. package/src/ui/docs/content/copyable.md +12 -3
  139. package/src/ui/docs/content/customization.md +5 -5
  140. package/src/ui/docs/content/cycle.md +3 -3
  141. package/src/ui/docs/content/data-state.md +11 -12
  142. package/src/ui/docs/content/data.md +26 -33
  143. package/src/ui/docs/content/detail.md +22 -4
  144. package/src/ui/docs/content/dialog.md +339 -31
  145. package/src/ui/docs/content/dictionary-value.md +17 -10
  146. package/src/ui/docs/content/dock.md +11 -3
  147. package/src/ui/docs/content/dot.md +8 -0
  148. package/src/ui/docs/content/drawer.md +27 -14
  149. package/src/ui/docs/content/empty-value.md +2 -2
  150. package/src/ui/docs/content/empty.md +19 -12
  151. package/src/ui/docs/content/events.md +4 -4
  152. package/src/ui/docs/content/field.md +34 -12
  153. package/src/ui/docs/content/getting-started.md +3 -3
  154. package/src/ui/docs/content/icon-picker.md +19 -4
  155. package/src/ui/docs/content/input-otp.md +20 -12
  156. package/src/ui/docs/content/input.md +121 -9
  157. package/src/ui/docs/content/item.md +27 -13
  158. package/src/ui/docs/content/kbd.md +19 -11
  159. package/src/ui/docs/content/label.md +12 -3
  160. package/src/ui/docs/content/log.md +4 -4
  161. package/src/ui/docs/content/markdown.md +7 -6
  162. package/src/ui/docs/content/mcp.md +13 -15
  163. package/src/ui/docs/content/menu.md +40 -16
  164. package/src/ui/docs/content/metric-card.md +13 -0
  165. package/src/ui/docs/content/observability.md +2 -2
  166. package/src/ui/docs/content/page.md +59 -6
  167. package/src/ui/docs/content/pagination.md +22 -17
  168. package/src/ui/docs/content/popover.md +22 -8
  169. package/src/ui/docs/content/progress.md +15 -16
  170. package/src/ui/docs/content/queue.md +5 -5
  171. package/src/ui/docs/content/radio-group.md +20 -12
  172. package/src/ui/docs/content/router.md +11 -6
  173. package/src/ui/docs/content/scheduler.md +4 -5
  174. package/src/ui/docs/content/scroll-area.md +12 -7
  175. package/src/ui/docs/content/select.md +47 -34
  176. package/src/ui/docs/content/semantic-context.md +2 -2
  177. package/src/ui/docs/content/separator.md +5 -5
  178. package/src/ui/docs/content/sidebar.md +329 -54
  179. package/src/ui/docs/content/skeleton.md +9 -2
  180. package/src/ui/docs/content/slider.md +8 -7
  181. package/src/ui/docs/content/spinner.md +8 -8
  182. package/src/ui/docs/content/split.md +29 -5
  183. package/src/ui/docs/content/storage.md +6 -8
  184. package/src/ui/docs/content/switch.md +8 -7
  185. package/src/ui/docs/content/table.md +13 -3
  186. package/src/ui/docs/content/tabs.md +28 -14
  187. package/src/ui/docs/content/testing.md +9 -11
  188. package/src/ui/docs/content/textarea.md +12 -4
  189. package/src/ui/docs/content/toast.md +47 -13
  190. package/src/ui/docs/content/toggle.md +75 -7
  191. package/src/ui/docs/content/tokens.md +7 -7
  192. package/src/ui/docs/content/tooltip.md +19 -11
  193. package/src/ui/docs/content/truncate.md +15 -8
  194. package/src/ui/docs/content/ui.md +24 -9
  195. package/src/ui/docs/content/upgrading.md +7 -8
  196. package/src/ui/docs/doc-client.tsx +5 -5
  197. package/src/ui/docs/doc.tsx +26 -14
  198. package/src/ui/docs/registry.tsx +25 -42
  199. package/src/ui/docs/standalone.tsx +2 -2
  200. package/src/ui/drivers/react.tsx +17 -12
  201. package/src/ui/lib/action-errors.ts +45 -0
  202. package/src/ui/lib/zod-pt-br.ts +31 -4
  203. package/src/ui/meta.ts +65 -95
  204. package/src/ui/react.tsx +17 -16
  205. package/src/ui/theme.css +60 -8
  206. package/src/vite/design.ts +6 -18
  207. package/src/ui/components/primitives/alert-dialog.tsx +0 -192
  208. package/src/ui/docs/content/alert-dialog.md +0 -73
  209. package/src/ui/docs/content/button-group.md +0 -71
  210. package/src/ui/docs/content/confirm.md +0 -120
  211. package/src/ui/docs/content/input-group.md +0 -79
  212. package/src/ui/docs/content/page-state.md +0 -45
  213. package/src/ui/docs/content/toggle-group.md +0 -81
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: invalid syntax '${src}'`)
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 error: unterminated string at ${startPos}`)
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 error: unexpected character '${c}' at position ${i}`,
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 error: expected ${value ?? type}, got '${t.value}' at ${t.pos}`,
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 error: invalid number '${t.value}'`)
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 error: unexpected token '${t.value}' at ${t.pos}`,
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 ?? '0.0.0' },
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('tbdlib-jobs', { connection: redisOpts })
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 → tbdlib JobStatus.
142
+ * BullMQ state → Opus JobStatus.
143
143
  *
144
- * tbdlib não tem 'paused' nem 'delayed' separados — todos viram 'queued'.
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
- `Schedule "${spec.name}" must declare either 'cron' or 'every'.`,
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(`[tbdlib/scheduler] schedule "${schedule.name}" fire failed:`, err)
93
+ console.error(`[opus/scheduler] falha ao disparar o schedule "${schedule.name}":`, err)
93
94
  }
@@ -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
- `Invalid 'every' shorthand: "${every}". ` +
19
- `Expected formato "<n><s|m|h|d>" (ex: "15s", "5m", "1h", "1d").`,
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' value must be > 0, got "${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' seconds must be <= 59, got ${n}.`)
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' minutes must be <= 59, got ${n}.`)
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' hours must be <= 23, got ${n}.`)
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' days only supports 1d. For longer intervals use cron.`)
41
+ throw new Error(`Dias em 'every' aceitam somente 1d. Para intervalos maiores, use cron.`)
42
42
  }
43
43
  return '0 0 * * *'
44
44
  }
@@ -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: 'Success',
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('Client error', action.errors),
186
- '5XX': errorResponse('Server error'),
185
+ '4XX': errorResponse('Erro do cliente', action.errors),
186
+ '5XX': errorResponse('Erro do servidor'),
187
187
  }
188
188
  return responses
189
189
  }
package/src/seed/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * implementação server-only que a CLI carrega pelo `opus.config.ts`.
6
6
  */
7
7
 
8
+ /** Variante de dados que um seed sabe aplicar (ex.: `minimal`, `demo`). */
8
9
  export interface SeedProfile {
9
10
  /** Situação que este perfil representa para quem vai aplicá-lo. */
10
11
  description: string
@@ -12,13 +13,16 @@ export interface SeedProfile {
12
13
  expected?: Readonly<Record<string, number>>
13
14
  }
14
15
 
16
+ /** Limites de onde o seed pode rodar. */
15
17
  export interface SeedSafety {
16
18
  /** Escopos de dados em que o seed pode abrir conexão. */
17
19
  scopes: readonly SeedScope[]
18
20
  }
19
21
 
22
+ /** Escopos de banco em que seeds podem abrir conexão; nunca produção. */
20
23
  export type SeedScope = 'local' | 'test' | 'isolated-preview'
21
24
 
25
+ /** Declaração pura de um seed — o que `defineSeed` valida e o manifesto publica. */
22
26
  export interface SeedDefinition {
23
27
  /** Identidade estável, em segmentos lowercase separados por ponto. */
24
28
  name: string
@@ -31,12 +35,14 @@ export interface SeedDefinition {
31
35
  safety: SeedSafety
32
36
  }
33
37
 
38
+ /** Contexto entregue a `plan`, `apply` e `verify` do binding. */
34
39
  export interface SeedContext<Database = unknown> {
35
40
  database: Database | null
36
41
  profile: string
37
42
  scope: SeedScope
38
43
  }
39
44
 
45
+ /** Contexto da asserção de alvo, executada antes de qualquer binding. */
40
46
  export interface SeedTargetContext<Database = unknown> {
41
47
  database: Database | null
42
48
  scope: SeedScope
@@ -50,28 +56,34 @@ export type SeedTargetAssertion<Database = unknown> = (
50
56
  context: SeedTargetContext<Database>,
51
57
  ) => boolean | Promise<boolean>
52
58
 
59
+ /** Resultado de `plan`: o que a aplicação faria, sem tocar o banco. */
53
60
  export interface SeedPlan {
54
61
  summary: string
55
62
  operations: readonly string[]
56
63
  }
57
64
 
65
+ /** Resultado de `apply` e `verify`: resumo e métricas observadas. */
58
66
  export interface SeedReport {
59
67
  summary: string
60
68
  metrics: Readonly<Record<string, number>>
61
69
  }
62
70
 
71
+ /** Implementação server-only de um seed, ligada à declaração por `bindSeed`. */
63
72
  export interface SeedBinding<Database = unknown> {
64
73
  plan(context: SeedContext<Database>): SeedPlan | Promise<SeedPlan>
65
74
  apply(context: SeedContext<Database>): SeedReport | Promise<SeedReport>
66
75
  verify(context: SeedContext<Database>): SeedReport | Promise<SeedReport>
67
76
  }
68
77
 
78
+ /** Seed com declaração e binding — a forma que o registro em `opus.config.ts` consome. */
69
79
  export interface BoundSeed<Database = unknown> extends SeedDefinition {
70
80
  readonly binding: SeedBinding<Database>
71
81
  }
72
82
 
83
+ /** Item aceito no registro; declaração sem binding é diagnosticada como `seed.unbound`. */
73
84
  export type SeedRegistryItem = SeedDefinition | BoundSeed
74
85
 
86
+ /** Problema encontrado ao verificar o registro de seeds. */
75
87
  export interface SeedDiagnostic {
76
88
  code:
77
89
  | 'seed.duplicate'
@@ -82,6 +94,7 @@ export interface SeedDiagnostic {
82
94
  seed: string
83
95
  }
84
96
 
97
+ /** Resultado de `checkSeedRegistry`. */
85
98
  export interface SeedRegistryCheck {
86
99
  ok: boolean
87
100
  diagnostics: readonly SeedDiagnostic[]
@@ -94,6 +107,7 @@ const PROFILE_RE = /^[a-z][a-z0-9-]*$/
94
107
  const SCOPE_RE = /^[a-z][a-z0-9-]*$/
95
108
  const SEED_SCOPES = new Set<SeedScope>(['local', 'test', 'isolated-preview'])
96
109
 
110
+ /** Valida e devolve a declaração de um seed, preservando o tipo literal. Lança em declaração inválida. */
97
111
  export function defineSeed<const Definition extends SeedDefinition>(
98
112
  definition: Definition,
99
113
  ): Definition {
@@ -129,6 +143,7 @@ export function publicSeedDefinition(seed: SeedDefinition): SeedDefinition {
129
143
  }
130
144
  }
131
145
 
146
+ /** Liga a implementação server-only (`plan`, `apply`, `verify`) a uma declaração validada. */
132
147
  export function bindSeed<Database = unknown>(
133
148
  definition: SeedDefinition,
134
149
  binding: SeedBinding<Database>,
@@ -138,6 +153,7 @@ export function bindSeed<Database = unknown>(
138
153
  return { ...definition, binding }
139
154
  }
140
155
 
156
+ /** Type guard: `true` se o valor é uma declaração de seed válida. */
141
157
  export function isSeedDefinition(value: unknown): value is SeedDefinition {
142
158
  try {
143
159
  validateDefinition(value)
@@ -147,6 +163,7 @@ export function isSeedDefinition(value: unknown): value is SeedDefinition {
147
163
  }
148
164
  }
149
165
 
166
+ /** Type guard: `true` se o valor é um seed declarado e com binding completo. */
150
167
  export function isBoundSeed(value: unknown): value is BoundSeed {
151
168
  if (!isSeedDefinition(value)) return false
152
169
  try {
@@ -157,6 +174,10 @@ export function isBoundSeed(value: unknown): value is BoundSeed {
157
174
  }
158
175
  }
159
176
 
177
+ /**
178
+ * Verifica o registro de seeds: duplicidade, binding ausente, dependência
179
+ * inexistente e ciclo. Devolve a ordem topológica quando não há ciclo.
180
+ */
160
181
  export function checkSeedRegistry(items: readonly SeedRegistryItem[]): SeedRegistryCheck {
161
182
  const diagnostics: SeedDiagnostic[] = []
162
183
  const byName = new Map<string, BoundSeed>()
@@ -237,6 +258,7 @@ export function checkSeedRegistry(items: readonly SeedRegistryItem[]): SeedRegis
237
258
  }
238
259
  }
239
260
 
261
+ /** Resolve o perfil a aplicar (`requested` ou `defaultProfile`); lança se não existir no seed. */
240
262
  export function resolveSeedProfile(seed: SeedDefinition, requested?: string): string {
241
263
  const profile = requested ?? seed.defaultProfile
242
264
  if (!Object.hasOwn(seed.profiles, profile)) {
@@ -247,6 +269,10 @@ export function resolveSeedProfile(seed: SeedDefinition, requested?: string): st
247
269
  return profile
248
270
  }
249
271
 
272
+ /**
273
+ * Falha fechado antes de abrir conexão: exige `NODE_ENV` de desenvolvimento ou
274
+ * teste, escopo informado e permitido por `safety.scopes`.
275
+ */
250
276
  export function assertSeedExecutionAllowed(
251
277
  seed: SeedDefinition,
252
278
  scope: string | undefined,
@@ -274,6 +300,7 @@ function runtimeNodeEnvironment(): string | undefined {
274
300
  ).process?.env?.NODE_ENV
275
301
  }
276
302
 
303
+ /** Garante que o retorno de `plan` tem a forma `{ summary, operations }`. */
277
304
  export function validateSeedPlan(value: unknown): SeedPlan {
278
305
  if (!isRecord(value) || !isNonEmptyString(value.summary) || !Array.isArray(value.operations)) {
279
306
  throw new Error('plan precisa retornar { summary, operations }')
@@ -284,6 +311,7 @@ export function validateSeedPlan(value: unknown): SeedPlan {
284
311
  return value as unknown as SeedPlan
285
312
  }
286
313
 
314
+ /** Garante que o retorno de `apply`/`verify` tem a forma `{ summary, metrics }` com números finitos. */
287
315
  export function validateSeedReport(value: unknown): SeedReport {
288
316
  if (!isRecord(value) || !isNonEmptyString(value.summary) || !isMetrics(value.metrics)) {
289
317
  throw new Error('apply/verify precisa retornar { summary, metrics } com valores numéricos finitos')
@@ -291,6 +319,7 @@ export function validateSeedReport(value: unknown): SeedReport {
291
319
  return value as unknown as SeedReport
292
320
  }
293
321
 
322
+ /** Compara as métricas do relatório com `profiles[profile].expected`; lança listando as divergências. */
294
323
  export function assertSeedExpectedMetrics(
295
324
  seed: SeedDefinition,
296
325
  profile: string,
@@ -28,6 +28,7 @@ import type {
28
28
  RuntimeRef,
29
29
  ServerAdapter,
30
30
  } from '../../core/index.ts'
31
+ import { readPackageVersion } from '../../core/package-version.ts'
31
32
  import {
32
33
  toOpenAPISpec,
33
34
  type OpenAPIInfo,
@@ -58,7 +59,8 @@ export interface FastifyServerOptions {
58
59
 
59
60
  /**
60
61
  * Info do OpenAPI document (title, version, description). Quando ausente,
61
- * usa um default genérico — sobrescreva pra publicar API decente.
62
+ * usa `{ title: 'Opus API', version: <versão do SDK> }` — sobrescreva pra
63
+ * publicar a identidade da sua API.
62
64
  */
63
65
  openapiInfo?: OpenAPIInfo
64
66
 
@@ -73,11 +75,12 @@ export interface FastifyServerOptions {
73
75
  // Adapter factory
74
76
  // =============================================================================
75
77
 
78
+ /** ServerAdapter que monta as actions como rotas numa instância Fastify existente. */
76
79
  export function fastifyServer(options: FastifyServerOptions): ServerAdapter {
77
80
  const {
78
81
  app,
79
82
  apiPrefix = '/api',
80
- openapiInfo = { title: 'tbdlib API', version: '0.0.0' },
83
+ openapiInfo = { title: 'Opus API', version: readPackageVersion() },
81
84
  openapiServers,
82
85
  traceContext = false,
83
86
  } = options
@@ -34,7 +34,8 @@ import type {
34
34
  RuntimeRef,
35
35
  ServerAdapter,
36
36
  } from '../../core/index.ts'
37
- import { error } from '../../core/index.ts'
37
+ import { error, UNHANDLED_ERROR_MESSAGE } from '../../core/index.ts'
38
+ import { readPackageVersion } from '../../core/package-version.ts'
38
39
  import {
39
40
  toOpenAPISpec,
40
41
  type OpenAPIInfo,
@@ -68,7 +69,8 @@ export interface NodeServerOptions {
68
69
 
69
70
  /**
70
71
  * Info do OpenAPI document (title, version, description). Quando ausente,
71
- * usa um default genérico — sobrescreva pra publicar API decente.
72
+ * usa `{ title: 'Opus API', version: <versão do SDK> }` — sobrescreva pra
73
+ * publicar a identidade da sua API.
72
74
  */
73
75
  openapiInfo?: OpenAPIInfo
74
76
 
@@ -97,11 +99,12 @@ export interface NodeServerHandle {
97
99
 
98
100
  type RouteFn = (req: IncomingMessage, res: ServerResponse, url: URL) => Promise<void>
99
101
 
102
+ /** ServerAdapter sobre `node:http` puro, sem framework; expõe `handler` pra plugar em qualquer servidor. */
100
103
  export function nodeServer(options: NodeServerOptions = {}): NodeServerHandle {
101
104
  const {
102
105
  apiPrefix = '/api',
103
106
  endpoints,
104
- openapiInfo = { title: 'tbdlib API', version: '0.0.0' },
107
+ openapiInfo = { title: 'Opus API', version: readPackageVersion() },
105
108
  openapiServers,
106
109
  traceContext = false,
107
110
  } = options
@@ -164,7 +167,7 @@ export function nodeServer(options: NodeServerOptions = {}): NodeServerHandle {
164
167
  res,
165
168
  400,
166
169
  driverResult(
167
- { code: 'server.invalid_json', category: 'validation', message: 'Request body is not valid JSON' },
170
+ { code: 'server.invalid_json', category: 'validation', message: 'O corpo da requisição não é um JSON válido' },
168
171
  action.name,
169
172
  requestId,
170
173
  ),
@@ -205,7 +208,7 @@ export function nodeServer(options: NodeServerOptions = {}): NodeServerHandle {
205
208
  res,
206
209
  500,
207
210
  driverResult(
208
- { code: 'server.unhandled', category: 'internal', message: String(err) },
211
+ { code: 'server.unhandled', category: 'internal', message: UNHANDLED_ERROR_MESSAGE, cause: String(err) },
209
212
  deriveActionName(url.pathname, apiPrefix),
210
213
  crypto.randomUUID(),
211
214
  ),
@@ -319,7 +322,7 @@ function driverResult(
319
322
 
320
323
  function routeNotFound(pathname: string, apiPrefix: string): ActionResult<never> {
321
324
  return driverResult(
322
- { code: 'server.route_not_found', category: 'not_found', message: `No route for ${pathname}` },
325
+ { code: 'server.route_not_found', category: 'not_found', message: `Rota não encontrada: ${pathname}` },
323
326
  deriveActionName(pathname, apiPrefix),
324
327
  crypto.randomUUID(),
325
328
  )
@@ -181,7 +181,9 @@ function serializeError(err: ActionError): Record<string, unknown> {
181
181
  if (err.i18nParams !== undefined) out.i18nParams = err.i18nParams
182
182
  if (err.field !== undefined) out.field = err.field
183
183
  if (err.issues !== undefined) out.issues = err.issues
184
- if (err.cause !== undefined) out.cause = String(err.cause)
184
+ // Erro interno não leva `cause` para a wire: é o texto cru da exceção (driver, SQL), que
185
+ // pertence ao log do servidor, não à pessoa nem ao cliente.
186
+ if (err.cause !== undefined && err.category !== 'internal') out.cause = String(err.cause)
185
187
  if (err.meta !== undefined) out.meta = err.meta
186
188
  return out
187
189
  }
@@ -29,6 +29,7 @@ function metaPathOf(filePath: string): string {
29
29
  return `${filePath}.meta.json`
30
30
  }
31
31
 
32
+ /** StorageAdapter que grava objetos no sistema de arquivos local, com metadados em `<arquivo>.meta.json`. */
32
33
  export function fsStorage(options: FsStorageOptions): StorageAdapter {
33
34
  const root = path.resolve(options.root)
34
35
  const baseUrl = options.baseUrl?.replace(/\/+$/, '')
@@ -250,7 +250,7 @@ async function validate<T>(
250
250
  throw error({
251
251
  code: kind === 'input' ? 'validation.invalid_input' : 'validation.invalid_output',
252
252
  category: kind === 'input' ? 'validation' : 'internal',
253
- message: kind === 'input' ? 'Input validation failed' : 'Output validation failed (dev)',
253
+ message: kind === 'input' ? 'Dados de entrada inválidos' : 'Dados de saída inválidos (validação em dev)',
254
254
  issues: result.issues.map((iss) => ({
255
255
  path: (iss.path ?? []).join('.'),
256
256
  code: 'invalid',
@@ -294,7 +294,7 @@ export async function runAction<In, Out, ParsedIn = In>(
294
294
  throw error({
295
295
  code: 'auth.unauthenticated',
296
296
  category: 'authentication',
297
- message: 'Authentication required',
297
+ message: 'Autenticação necessária',
298
298
  })
299
299
  }
300
300
 
@@ -305,7 +305,7 @@ export async function runAction<In, Out, ParsedIn = In>(
305
305
  ? Boolean(evalExpression(parseExpression(spec), { user: ctx.user, input: parsed, ctx, ...(loaded ?? {}) }))
306
306
  : await spec(ctx, parsed, loaded)
307
307
  if (decision === false) {
308
- throw error({ code: 'auth.forbidden', category: 'authorization', message: 'Forbidden' })
308
+ throw error({ code: 'auth.forbidden', category: 'authorization', message: 'Acesso negado' })
309
309
  }
310
310
  if (decision !== true) throw decision
311
311
  }