@softize/opus 8.6.6

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 (286) hide show
  1. package/CHANGELOG.md +1616 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/bin/cli.mjs +528 -0
  5. package/bin/lib/check.mjs +307 -0
  6. package/bin/lib/components.mjs +151 -0
  7. package/bin/lib/create.mjs +208 -0
  8. package/bin/lib/db-check-runner.mjs +86 -0
  9. package/bin/lib/db-migrate-runner.mjs +89 -0
  10. package/bin/lib/db-scaffold-runner.mjs +84 -0
  11. package/bin/lib/db.mjs +261 -0
  12. package/bin/lib/docs-include.mjs +48 -0
  13. package/bin/lib/gen-dicts.mjs +134 -0
  14. package/bin/lib/gen-docs.mjs +288 -0
  15. package/bin/lib/gen-manifest.mjs +102 -0
  16. package/bin/lib/gen-openapi.mjs +195 -0
  17. package/bin/lib/gen-runner.mjs +472 -0
  18. package/bin/lib/gen-stubs.mjs +463 -0
  19. package/bin/lib/gen.mjs +311 -0
  20. package/bin/lib/init.mjs +514 -0
  21. package/bin/lib/introspect.mjs +107 -0
  22. package/bin/lib/mcp.mjs +85 -0
  23. package/bin/lib/postinstall.mjs +56 -0
  24. package/docs/chat-event-protocol.md +85 -0
  25. package/docs/code-style.md +16 -0
  26. package/docs/data-layer.md +246 -0
  27. package/docs/ownership-vs-shadcn-lock.md +102 -0
  28. package/docs/protocol.md +2053 -0
  29. package/docs/releasing.md +110 -0
  30. package/docs/shellnav.md +131 -0
  31. package/package.json +338 -0
  32. package/registry/hooks/hooks.json +26 -0
  33. package/registry/hooks/link-memory-on-start.mjs +46 -0
  34. package/registry/hooks/opus-check-on-stop.mjs +114 -0
  35. package/registry/skills/create-action/SKILL.md +49 -0
  36. package/registry/skills/create-action/scaffold.mjs +122 -0
  37. package/registry/templates/app/_gitignore +3 -0
  38. package/registry/templates/app/_npmrc +1 -0
  39. package/registry/templates/app/_opus/_gitignore +5 -0
  40. package/registry/templates/app/_prettierrc.json +6 -0
  41. package/registry/templates/app/index.html +13 -0
  42. package/registry/templates/app/opus.config.ts +16 -0
  43. package/registry/templates/app/package.json +43 -0
  44. package/registry/templates/app/pnpm-workspace.yaml +11 -0
  45. package/registry/templates/app/public/favicon.svg +4 -0
  46. package/registry/templates/app/src/App.tsx +37 -0
  47. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
  48. package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
  49. package/registry/templates/app/src/domains/tasks/index.ts +13 -0
  50. package/registry/templates/app/src/index.css +18 -0
  51. package/registry/templates/app/src/main.tsx +25 -0
  52. package/registry/templates/app/tsconfig.json +20 -0
  53. package/registry/templates/app/vite.config.ts +46 -0
  54. package/registry/templates/monorepo/_gitignore +3 -0
  55. package/registry/templates/monorepo/_npmrc +1 -0
  56. package/registry/templates/monorepo/package.json +9 -0
  57. package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
  58. package/src/ai/ask.ts +64 -0
  59. package/src/ai/drivers/anthropic.ts +309 -0
  60. package/src/ai/index.ts +17 -0
  61. package/src/audit/drivers/console.ts +117 -0
  62. package/src/audit/drivers/pg.ts +172 -0
  63. package/src/audit/index.ts +51 -0
  64. package/src/auth/drivers/better-auth.ts +103 -0
  65. package/src/auth/drivers/jwt.ts +188 -0
  66. package/src/auth/index.ts +9 -0
  67. package/src/client/drivers/fetch.ts +202 -0
  68. package/src/client/index.ts +22 -0
  69. package/src/core/actions.ts +110 -0
  70. package/src/core/audit.ts +239 -0
  71. package/src/core/contracts.ts +137 -0
  72. package/src/core/domain.ts +310 -0
  73. package/src/core/errors.ts +181 -0
  74. package/src/core/index.ts +174 -0
  75. package/src/core/logical-type.ts +31 -0
  76. package/src/core/reactions.ts +81 -0
  77. package/src/core/runtime.ts +1167 -0
  78. package/src/core/schedules.ts +41 -0
  79. package/src/core/types.ts +1356 -0
  80. package/src/data/drivers/kysely.ts +389 -0
  81. package/src/data/index.ts +10 -0
  82. package/src/data/readonly-pool.ts +160 -0
  83. package/src/dsl/eval.ts +136 -0
  84. package/src/dsl/index.ts +29 -0
  85. package/src/dsl/kysely.ts +230 -0
  86. package/src/dsl/loads.ts +123 -0
  87. package/src/dsl/parser.ts +423 -0
  88. package/src/dsl/types.ts +113 -0
  89. package/src/events/drivers/mitt.ts +70 -0
  90. package/src/events/index.ts +9 -0
  91. package/src/log/drivers/pino.ts +57 -0
  92. package/src/log/index.ts +9 -0
  93. package/src/mcp/index.ts +62 -0
  94. package/src/queue/drivers/bullmq.ts +190 -0
  95. package/src/queue/index.ts +9 -0
  96. package/src/scheduler/drivers/node-cron.ts +93 -0
  97. package/src/scheduler/every.ts +45 -0
  98. package/src/scheduler/index.ts +9 -0
  99. package/src/schema/drivers/zod.ts +765 -0
  100. package/src/schema/entity.ts +439 -0
  101. package/src/schema/format/locale.ts +144 -0
  102. package/src/schema/index.ts +65 -0
  103. package/src/schema/openapi.ts +302 -0
  104. package/src/schema/scaffold.ts +160 -0
  105. package/src/server/drivers/fastify.ts +224 -0
  106. package/src/server/drivers/node.ts +386 -0
  107. package/src/server/index.ts +142 -0
  108. package/src/storage/drivers/fs.ts +90 -0
  109. package/src/storage/drivers/s3.ts +117 -0
  110. package/src/storage/index.ts +27 -0
  111. package/src/testing/fake.ts +298 -0
  112. package/src/testing/index.ts +324 -0
  113. package/src/ui/components/patterns/action-form-card.tsx +48 -0
  114. package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
  115. package/src/ui/components/patterns/app-shell.tsx +227 -0
  116. package/src/ui/components/patterns/confirm.tsx +226 -0
  117. package/src/ui/components/patterns/data-state.tsx +75 -0
  118. package/src/ui/components/patterns/form-dialog.tsx +64 -0
  119. package/src/ui/components/patterns/form.tsx +584 -0
  120. package/src/ui/components/patterns/list.tsx +1488 -0
  121. package/src/ui/components/patterns/page.tsx +46 -0
  122. package/src/ui/components/patterns/section-shell.tsx +246 -0
  123. package/src/ui/components/patterns/shell-nav.tsx +150 -0
  124. package/src/ui/components/patterns/sidebar.tsx +89 -0
  125. package/src/ui/components/patterns/split.tsx +93 -0
  126. package/src/ui/components/patterns/trigger.tsx +196 -0
  127. package/src/ui/components/patterns/view.tsx +84 -0
  128. package/src/ui/components/primitives/accordion.tsx +64 -0
  129. package/src/ui/components/primitives/alert-dialog.tsx +190 -0
  130. package/src/ui/components/primitives/alert.tsx +116 -0
  131. package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
  132. package/src/ui/components/primitives/avatar.tsx +107 -0
  133. package/src/ui/components/primitives/badge.tsx +37 -0
  134. package/src/ui/components/primitives/breadcrumb.tsx +109 -0
  135. package/src/ui/components/primitives/button-group.tsx +83 -0
  136. package/src/ui/components/primitives/button.tsx +102 -0
  137. package/src/ui/components/primitives/calendar.tsx +218 -0
  138. package/src/ui/components/primitives/card.tsx +56 -0
  139. package/src/ui/components/primitives/carousel.tsx +239 -0
  140. package/src/ui/components/primitives/chat.tsx +407 -0
  141. package/src/ui/components/primitives/checkbox.tsx +30 -0
  142. package/src/ui/components/primitives/collapsible.tsx +31 -0
  143. package/src/ui/components/primitives/command.tsx +182 -0
  144. package/src/ui/components/primitives/composer.tsx +121 -0
  145. package/src/ui/components/primitives/copyable.tsx +50 -0
  146. package/src/ui/components/primitives/dialog.tsx +147 -0
  147. package/src/ui/components/primitives/drawer.tsx +141 -0
  148. package/src/ui/components/primitives/empty.tsx +104 -0
  149. package/src/ui/components/primitives/field.tsx +246 -0
  150. package/src/ui/components/primitives/icon-picker.tsx +180 -0
  151. package/src/ui/components/primitives/input-group.tsx +168 -0
  152. package/src/ui/components/primitives/input-otp.tsx +75 -0
  153. package/src/ui/components/primitives/input.tsx +72 -0
  154. package/src/ui/components/primitives/item.tsx +193 -0
  155. package/src/ui/components/primitives/kbd.tsx +28 -0
  156. package/src/ui/components/primitives/label.tsx +22 -0
  157. package/src/ui/components/primitives/markdown.tsx +35 -0
  158. package/src/ui/components/primitives/menu.tsx +255 -0
  159. package/src/ui/components/primitives/pagination.tsx +127 -0
  160. package/src/ui/components/primitives/popover.tsx +87 -0
  161. package/src/ui/components/primitives/progress.tsx +29 -0
  162. package/src/ui/components/primitives/radio-group.tsx +43 -0
  163. package/src/ui/components/primitives/resizable.tsx +51 -0
  164. package/src/ui/components/primitives/scroll-area.tsx +56 -0
  165. package/src/ui/components/primitives/select.tsx +479 -0
  166. package/src/ui/components/primitives/separator.tsx +26 -0
  167. package/src/ui/components/primitives/skeleton.tsx +13 -0
  168. package/src/ui/components/primitives/slider.tsx +61 -0
  169. package/src/ui/components/primitives/sonner.tsx +46 -0
  170. package/src/ui/components/primitives/spinner.tsx +29 -0
  171. package/src/ui/components/primitives/switch.tsx +33 -0
  172. package/src/ui/components/primitives/table.tsx +114 -0
  173. package/src/ui/components/primitives/tabs.tsx +104 -0
  174. package/src/ui/components/primitives/textarea.tsx +18 -0
  175. package/src/ui/components/primitives/toggle-group.tsx +81 -0
  176. package/src/ui/components/primitives/toggle.tsx +45 -0
  177. package/src/ui/components/primitives/tooltip.tsx +55 -0
  178. package/src/ui/components/primitives/truncate.tsx +49 -0
  179. package/src/ui/docs/DocBrowser.tsx +90 -0
  180. package/src/ui/docs/changelog.tsx +80 -0
  181. package/src/ui/docs/content/accordion.md +86 -0
  182. package/src/ui/docs/content/action-form-card.md +24 -0
  183. package/src/ui/docs/content/action-form-dialog.md +30 -0
  184. package/src/ui/docs/content/action-form.md +125 -0
  185. package/src/ui/docs/content/action-list-dialog.md +68 -0
  186. package/src/ui/docs/content/action-list.md +194 -0
  187. package/src/ui/docs/content/action-trigger.md +72 -0
  188. package/src/ui/docs/content/action-view.md +47 -0
  189. package/src/ui/docs/content/actions.md +138 -0
  190. package/src/ui/docs/content/ai.md +112 -0
  191. package/src/ui/docs/content/alert-dialog.md +73 -0
  192. package/src/ui/docs/content/alert.md +69 -0
  193. package/src/ui/docs/content/app-shell.md +155 -0
  194. package/src/ui/docs/content/aspect-ratio.md +66 -0
  195. package/src/ui/docs/content/audit.md +84 -0
  196. package/src/ui/docs/content/auth.md +70 -0
  197. package/src/ui/docs/content/avatar.md +94 -0
  198. package/src/ui/docs/content/badge.md +48 -0
  199. package/src/ui/docs/content/breadcrumb.md +87 -0
  200. package/src/ui/docs/content/button-group.md +71 -0
  201. package/src/ui/docs/content/button.md +60 -0
  202. package/src/ui/docs/content/calendar.md +62 -0
  203. package/src/ui/docs/content/card.md +49 -0
  204. package/src/ui/docs/content/carousel.md +85 -0
  205. package/src/ui/docs/content/chat.md +69 -0
  206. package/src/ui/docs/content/checkbox.md +75 -0
  207. package/src/ui/docs/content/cli.md +58 -0
  208. package/src/ui/docs/content/collapsible.md +64 -0
  209. package/src/ui/docs/content/command.md +56 -0
  210. package/src/ui/docs/content/composer.md +50 -0
  211. package/src/ui/docs/content/confirm.md +120 -0
  212. package/src/ui/docs/content/copyable.md +30 -0
  213. package/src/ui/docs/content/customization.md +110 -0
  214. package/src/ui/docs/content/cycle.md +34 -0
  215. package/src/ui/docs/content/data-state.md +47 -0
  216. package/src/ui/docs/content/data.md +99 -0
  217. package/src/ui/docs/content/dialog.md +60 -0
  218. package/src/ui/docs/content/drawer.md +55 -0
  219. package/src/ui/docs/content/empty.md +66 -0
  220. package/src/ui/docs/content/events.md +61 -0
  221. package/src/ui/docs/content/field.md +58 -0
  222. package/src/ui/docs/content/getting-started.md +109 -0
  223. package/src/ui/docs/content/icon-picker.md +51 -0
  224. package/src/ui/docs/content/input-group.md +78 -0
  225. package/src/ui/docs/content/input-otp.md +72 -0
  226. package/src/ui/docs/content/input.md +78 -0
  227. package/src/ui/docs/content/item.md +84 -0
  228. package/src/ui/docs/content/kbd.md +62 -0
  229. package/src/ui/docs/content/label.md +32 -0
  230. package/src/ui/docs/content/log.md +55 -0
  231. package/src/ui/docs/content/markdown.md +41 -0
  232. package/src/ui/docs/content/mcp.md +44 -0
  233. package/src/ui/docs/content/menu.md +114 -0
  234. package/src/ui/docs/content/microcopy.md +83 -0
  235. package/src/ui/docs/content/page.md +34 -0
  236. package/src/ui/docs/content/pagination.md +99 -0
  237. package/src/ui/docs/content/popover.md +49 -0
  238. package/src/ui/docs/content/progress.md +69 -0
  239. package/src/ui/docs/content/queue.md +62 -0
  240. package/src/ui/docs/content/radio-group.md +77 -0
  241. package/src/ui/docs/content/resizable.md +86 -0
  242. package/src/ui/docs/content/router.md +56 -0
  243. package/src/ui/docs/content/runtime.md +77 -0
  244. package/src/ui/docs/content/scheduler.md +66 -0
  245. package/src/ui/docs/content/scroll-area.md +89 -0
  246. package/src/ui/docs/content/section-shell.md +121 -0
  247. package/src/ui/docs/content/select.md +342 -0
  248. package/src/ui/docs/content/separator.md +33 -0
  249. package/src/ui/docs/content/sidebar.md +38 -0
  250. package/src/ui/docs/content/skeleton.md +34 -0
  251. package/src/ui/docs/content/slider.md +64 -0
  252. package/src/ui/docs/content/spinner.md +37 -0
  253. package/src/ui/docs/content/split.md +33 -0
  254. package/src/ui/docs/content/storage.md +69 -0
  255. package/src/ui/docs/content/switch.md +69 -0
  256. package/src/ui/docs/content/table.md +102 -0
  257. package/src/ui/docs/content/tabs.md +94 -0
  258. package/src/ui/docs/content/testing.md +89 -0
  259. package/src/ui/docs/content/textarea.md +30 -0
  260. package/src/ui/docs/content/toast.md +67 -0
  261. package/src/ui/docs/content/toggle-group.md +81 -0
  262. package/src/ui/docs/content/toggle.md +72 -0
  263. package/src/ui/docs/content/tokens.md +171 -0
  264. package/src/ui/docs/content/tooltip.md +50 -0
  265. package/src/ui/docs/content/truncate.md +37 -0
  266. package/src/ui/docs/content/ui.md +40 -0
  267. package/src/ui/docs/content/upgrading.md +48 -0
  268. package/src/ui/docs/doc-client.tsx +214 -0
  269. package/src/ui/docs/doc.tsx +301 -0
  270. package/src/ui/docs/folder.tsx +149 -0
  271. package/src/ui/docs/index.ts +21 -0
  272. package/src/ui/docs/markdown.tsx +130 -0
  273. package/src/ui/docs/md-raw.d.ts +4 -0
  274. package/src/ui/docs/plugin.ts +104 -0
  275. package/src/ui/docs/registry.tsx +424 -0
  276. package/src/ui/docs/standalone.tsx +107 -0
  277. package/src/ui/drivers/react.tsx +627 -0
  278. package/src/ui/index.ts +92 -0
  279. package/src/ui/lib/cn.ts +10 -0
  280. package/src/ui/lib/zod-pt-br.ts +38 -0
  281. package/src/ui/meta.ts +412 -0
  282. package/src/ui/react.tsx +235 -0
  283. package/src/ui/router.ts +96 -0
  284. package/src/ui/theme.css +234 -0
  285. package/src/vite/design.ts +652 -0
  286. package/src/vite/index.ts +8 -0
@@ -0,0 +1,1167 @@
1
+ /**
2
+ * tbdlib — Runtime
3
+ *
4
+ * Orquestrador central. Recebe actions + reactions + adapters + config no setup,
5
+ * registra cada peça, e executa o pipeline padrão (validate → load → auth →
6
+ * authorize → handler → audit → result).
7
+ *
8
+ * Pipeline interno é privado nesta versão (v0). Se crescer, fatorar em
9
+ * `pipeline.ts` separado.
10
+ *
11
+ * Ver §2, §3, §4, §5, §8, §14 do protocolo.
12
+ */
13
+
14
+ import { isReaction } from './reactions.ts'
15
+ import { isSchedule } from './schedules.ts'
16
+ import { collectDomainWarnings, flattenDomain, isDomainConfig } from './domain.ts'
17
+ import type { DomainConfig } from './domain.ts'
18
+ import { AuditEmitter } from './audit.ts'
19
+ import { error, isActionError, normalizeError } from './errors.ts'
20
+ import { evalExpression } from '../dsl/eval.ts'
21
+ import { parseExpression } from '../dsl/parser.ts'
22
+ import { parseLoad, resolveLoadArgs } from '../dsl/loads.ts'
23
+ import type { AstNode } from '../dsl/types.ts'
24
+ import type { LoadSpec as DslLoadSpec } from '../dsl/loads.ts'
25
+ import type {
26
+ ActionContext,
27
+ ActionDef,
28
+ ActionResult,
29
+ Adapter,
30
+ AuditRecord,
31
+ AuditSink,
32
+ AuthAdapter,
33
+ AuthorizeFn,
34
+ AuthorizeSpec,
35
+ BackoffSpec,
36
+ ClientAdapter,
37
+ ContextBase,
38
+ DataAdapter,
39
+ DomainEvent,
40
+ EventBusAdapter,
41
+ HealthStatus,
42
+ I18nRef,
43
+ LoaderFn,
44
+ LoaderResolver,
45
+ LoadsSpec,
46
+ Logger,
47
+ LoggerAdapter,
48
+ Provenance,
49
+ QueueAdapter,
50
+ ReactionContext,
51
+ ReactionDef,
52
+ ResultMeta,
53
+ Schema,
54
+ ScheduleDef,
55
+ SchedulerAdapter,
56
+ ServerAdapter,
57
+ StorageAdapter,
58
+ AiAdapter,
59
+ AiRunOptions,
60
+ BoundAiRunOptions,
61
+ BoundAi,
62
+ AiTool,
63
+ AIConfig,
64
+ User,
65
+ } from './types.ts'
66
+
67
+ /* eslint-disable @typescript-eslint/no-explicit-any */
68
+
69
+ /** Normaliza o `ai` da action (`boolean | AIConfig`) → config, ou null se não exposta à IA. */
70
+ function normalizeAiConfig(ai: boolean | AIConfig | undefined): AIConfig | null {
71
+ if (ai === undefined || ai === false) return null
72
+ if (ai === true) return { enabled: true }
73
+ return ai.enabled === false ? null : ai
74
+ }
75
+
76
+ // =============================================================================
77
+ // Setup types
78
+ // =============================================================================
79
+
80
+ export interface RuntimeSetup {
81
+ server?: ServerAdapter
82
+ data?: DataAdapter
83
+ auth?: AuthAdapter
84
+ logger?: LoggerAdapter
85
+ audit?: AuditSink | AuditSink[]
86
+ queue?: QueueAdapter
87
+ eventBus?: EventBusAdapter
88
+ scheduler?: SchedulerAdapter
89
+ client?: ClientAdapter
90
+ /** EXPERIMENTAL — storage de objetos (arquivos); chega nos handlers via `ctx.storage`. */
91
+ storage?: StorageAdapter
92
+ /** EXPERIMENTAL — IA generativa (complete/extract); chega nos handlers via `ctx.ai`. */
93
+ ai?: AiAdapter
94
+
95
+ config?: RuntimeSetupConfig
96
+ }
97
+
98
+ /**
99
+ * Subset de `RuntimeConfig` que o usuário passa. Defaults completos são
100
+ * aplicados internamente em `applyConfigDefaults`.
101
+ */
102
+ export interface RuntimeSetupConfig {
103
+ auditMode?: 'lenient' | 'strict'
104
+ emitMode?: 'lenient' | 'strict'
105
+ env?: 'development' | 'production' | 'test'
106
+ dev?: boolean
107
+ validateOutputInDev?: boolean
108
+ warnUndeclaredEmits?: boolean
109
+ dedupCacheTtl?: number
110
+ defaultRetry?: { attempts: number; backoff?: BackoffSpec }
111
+ i18n?: {
112
+ resolver?: (ref: I18nRef, locale?: string) => string
113
+ defaultLocale?: string
114
+ }
115
+ maxConcurrentActions?: number
116
+ maxConcurrentReactions?: number
117
+ }
118
+
119
+ /**
120
+ * `RuntimeConfig` totalmente resolvido (com defaults aplicados).
121
+ * Acessível via `runtime.config` em adapters.
122
+ */
123
+ export interface ResolvedRuntimeConfig {
124
+ auditMode: 'lenient' | 'strict'
125
+ emitMode: 'lenient' | 'strict'
126
+ env: 'development' | 'production' | 'test'
127
+ dev: boolean
128
+ validateOutputInDev: boolean
129
+ warnUndeclaredEmits: boolean
130
+ dedupCacheTtl: number
131
+ defaultRetry: { attempts: number; backoff?: BackoffSpec }
132
+ i18n: {
133
+ resolver?: (ref: I18nRef, locale?: string) => string
134
+ defaultLocale: string
135
+ }
136
+ maxConcurrentActions: number
137
+ maxConcurrentReactions: number
138
+ }
139
+
140
+ // =============================================================================
141
+ // Factory
142
+ // =============================================================================
143
+
144
+ /**
145
+ * Cria um runtime. Wrapper conveniente sobre `new Runtime(setup)`.
146
+ */
147
+ export function createRuntime(setup: RuntimeSetup = {}): Runtime {
148
+ return new Runtime(setup)
149
+ }
150
+
151
+ // =============================================================================
152
+ // Default console logger
153
+ // =============================================================================
154
+
155
+ class ConsoleLogger implements Logger {
156
+ constructor(private readonly bindings: Readonly<Record<string, unknown>> = {}) {}
157
+
158
+ trace(msg: string, meta?: object): void {
159
+ this.write('trace', msg, meta)
160
+ }
161
+ debug(msg: string, meta?: object): void {
162
+ this.write('debug', msg, meta)
163
+ }
164
+ info(msg: string, meta?: object): void {
165
+ this.write('info', msg, meta)
166
+ }
167
+ warn(msg: string, meta?: object): void {
168
+ this.write('warn', msg, meta)
169
+ }
170
+ error(msg: string, meta?: object): void {
171
+ this.write('error', msg, meta)
172
+ }
173
+ fatal(msg: string, meta?: object): void {
174
+ this.write('fatal', msg, meta)
175
+ }
176
+
177
+ child(bindings: object): Logger {
178
+ return new ConsoleLogger({ ...this.bindings, ...(bindings as object) })
179
+ }
180
+
181
+ private write(level: string, msg: string, meta?: object): void {
182
+ const merged = { ...this.bindings, ...(meta ?? {}) }
183
+ const target = level === 'error' || level === 'fatal' ? console.error : console.log
184
+ /* v8 ignore next — bindings sempre não-vazias quando criado via Runtime */
185
+ target(`[${level}] ${msg}`, Object.keys(merged).length > 0 ? merged : '')
186
+ }
187
+ }
188
+
189
+ // =============================================================================
190
+ // Defaults
191
+ // =============================================================================
192
+
193
+ function applyConfigDefaults(c: RuntimeSetupConfig = {}): ResolvedRuntimeConfig {
194
+ const env = c.env ?? 'development'
195
+ const dev = c.dev ?? env !== 'production'
196
+ const i18n: ResolvedRuntimeConfig['i18n'] = {
197
+ defaultLocale: c.i18n?.defaultLocale ?? 'pt-BR',
198
+ }
199
+ if (c.i18n?.resolver !== undefined) i18n.resolver = c.i18n.resolver
200
+
201
+ return {
202
+ auditMode: c.auditMode ?? 'lenient',
203
+ emitMode: c.emitMode ?? 'lenient',
204
+ env,
205
+ dev,
206
+ validateOutputInDev: c.validateOutputInDev ?? dev,
207
+ warnUndeclaredEmits: c.warnUndeclaredEmits ?? dev,
208
+ dedupCacheTtl: c.dedupCacheTtl ?? 60_000,
209
+ defaultRetry: c.defaultRetry ?? {
210
+ attempts: 3,
211
+ backoff: { kind: 'exponential', initialMs: 1000 },
212
+ },
213
+ i18n,
214
+ maxConcurrentActions: c.maxConcurrentActions ?? Number.POSITIVE_INFINITY,
215
+ maxConcurrentReactions: c.maxConcurrentReactions ?? 10,
216
+ }
217
+ }
218
+
219
+ // =============================================================================
220
+ // Runtime
221
+ // =============================================================================
222
+
223
+ export class Runtime {
224
+ readonly config: ResolvedRuntimeConfig
225
+ readonly log: Logger
226
+ readonly audit: AuditEmitter
227
+ readonly auth: AuthAdapter | undefined
228
+
229
+ private readonly server: ServerAdapter | undefined
230
+ private readonly data: DataAdapter | undefined
231
+ private readonly logger: LoggerAdapter | undefined
232
+ private readonly queue: QueueAdapter | undefined
233
+ private readonly eventBus: EventBusAdapter | undefined
234
+ private readonly scheduler: SchedulerAdapter | undefined
235
+ private readonly client: ClientAdapter | undefined
236
+ private readonly storage: StorageAdapter | undefined
237
+ private readonly ai: AiAdapter | undefined
238
+ /** Cache das tools derivadas das actions `ai:enabled` (registry é estático pós-start). */
239
+ private aiToolsCache: AiTool[] | null = null
240
+
241
+ private readonly actions = new Map<string, ActionDef>()
242
+ private readonly reactions = new Map<string, ReactionDef<any>>()
243
+ private readonly schedules = new Map<string, ScheduleDef>()
244
+
245
+ /**
246
+ * Map entidade → resolver, usado pra carregar specs DSL de loads
247
+ * (`'ticket(:id)'`). Registrado via `registerLoaders`. Closures
248
+ * em `action.loads` não passam por aqui.
249
+ */
250
+ private readonly loaderResolvers = new Map<string, LoaderResolver>()
251
+
252
+ /**
253
+ * Cache de AST de DSL `authorize` indexado por string crua. Parsing é
254
+ * idempotente; evita refazer toda execução.
255
+ */
256
+ private readonly authorizeAstCache = new Map<string, AstNode>()
257
+
258
+ /**
259
+ * Cache equivalente pra DSL de `loads` — chave é o spec string crua;
260
+ * valor é o `LoadSpec` parseado.
261
+ */
262
+ private readonly loadSpecCache = new Map<string, DslLoadSpec>()
263
+
264
+ private started = false
265
+ private readonly disposeCallbacks: Array<() => Promise<void> | void> = []
266
+
267
+ constructor(setup: RuntimeSetup) {
268
+ this.config = applyConfigDefaults(setup.config)
269
+ this.logger = setup.logger
270
+ this.log = setup.logger ?? new ConsoleLogger({ runtime: 'tbdlib' })
271
+
272
+ this.audit = new AuditEmitter({ mode: this.config.auditMode, log: this.log })
273
+ if (setup.audit !== undefined) {
274
+ const sinks = Array.isArray(setup.audit) ? setup.audit : [setup.audit]
275
+ for (const sink of sinks) this.audit.register(sink)
276
+ }
277
+
278
+ this.server = setup.server
279
+ this.data = setup.data
280
+ this.auth = setup.auth
281
+ this.queue = setup.queue
282
+ this.eventBus = setup.eventBus
283
+ this.scheduler = setup.scheduler
284
+ this.client = setup.client
285
+ this.storage = setup.storage
286
+ this.ai = setup.ai
287
+ }
288
+
289
+ /**
290
+ * Registra actions, reactions, schedules e/ou domínios. Aceita lista mista;
291
+ * distinção por shape via `isSchedule`/`isReaction`/`isDomainConfig`.
292
+ *
293
+ * Quando recebe um `DomainConfig`, achata via `flattenDomain` e registra
294
+ * as peças resultantes — qualquer colisão de nome (incluindo entre domínios
295
+ * registrados separadamente) é detectada aqui.
296
+ */
297
+ register(
298
+ items: Array<ActionDef | ReactionDef<any> | ScheduleDef | DomainConfig>,
299
+ ): void {
300
+ for (const item of items) {
301
+ if (isDomainConfig(item)) {
302
+ collectDomainWarnings(item)
303
+ const { actions, reactions, schedules } = flattenDomain(item)
304
+ this.registerFlat(actions, reactions, schedules)
305
+ } else if (isSchedule(item)) {
306
+ this.registerSchedules([item])
307
+ } else if (isReaction(item)) {
308
+ this.registerReactions([item])
309
+ } else {
310
+ this.registerActions([item as ActionDef])
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Registra resolvers de entidades pra DSL `loads`.
317
+ *
318
+ * Quando uma action declara `loads: 'ticket(:id)'`, o runtime parsa o
319
+ * spec, resolve os args contra o input (`{ id: input.id }`) e chama o
320
+ * resolver registrado pra `ticket`.
321
+ *
322
+ * Closures em `action.loads` ignoram esse map — só specs string usam.
323
+ *
324
+ * Reregistrar a mesma entidade lança — fail-fast pra evitar shadow.
325
+ * Pra reset (testes), criar runtime novo.
326
+ */
327
+ registerLoaders(resolvers: Record<string, LoaderResolver>): void {
328
+ for (const [entity, resolver] of Object.entries(resolvers)) {
329
+ if (this.loaderResolvers.has(entity)) {
330
+ throw error({
331
+ code: 'runtime.duplicate_loader',
332
+ category: 'internal',
333
+ message: `Loader resolver "${entity}" already registered`,
334
+ })
335
+ }
336
+ this.loaderResolvers.set(entity, resolver)
337
+ }
338
+ }
339
+
340
+ private registerFlat(
341
+ actions: ActionDef[],
342
+ reactions: Array<ReactionDef<any>>,
343
+ schedules: ScheduleDef[],
344
+ ): void {
345
+ this.registerActions(actions)
346
+ this.registerReactions(reactions)
347
+ this.registerSchedules(schedules)
348
+ }
349
+
350
+ private registerActions(actions: ActionDef[]): void {
351
+ for (const action of actions) {
352
+ if (this.actions.has(action.name)) {
353
+ throw error({
354
+ code: 'runtime.duplicate_action',
355
+ category: 'internal',
356
+ message: `Action "${action.name}" registered twice`,
357
+ })
358
+ }
359
+ this.actions.set(action.name, action)
360
+ }
361
+ }
362
+
363
+ private registerReactions(reactions: Array<ReactionDef<any>>): void {
364
+ for (const reaction of reactions) {
365
+ if (this.reactions.has(reaction.name)) {
366
+ throw error({
367
+ code: 'runtime.duplicate_reaction',
368
+ category: 'internal',
369
+ message: `Reaction "${reaction.name}" registered twice`,
370
+ })
371
+ }
372
+ this.reactions.set(reaction.name, reaction)
373
+ }
374
+ }
375
+
376
+ private registerSchedules(schedules: ScheduleDef[]): void {
377
+ for (const schedule of schedules) {
378
+ if (this.schedules.has(schedule.name)) {
379
+ throw error({
380
+ code: 'runtime.duplicate_schedule',
381
+ category: 'internal',
382
+ message: `Schedule "${schedule.name}" registered twice`,
383
+ })
384
+ }
385
+ this.schedules.set(schedule.name, schedule)
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Inicializa adapters, monta actions no server, subscreve reactions no event bus.
391
+ * Fail-fast: erro em qualquer init aborta o boot.
392
+ */
393
+ async start(): Promise<void> {
394
+ if (this.started) {
395
+ throw error({
396
+ code: 'runtime.already_started',
397
+ category: 'internal',
398
+ message: 'Runtime.start() called twice',
399
+ })
400
+ }
401
+
402
+ for (const adapter of this.allAdapters()) {
403
+ if (adapter.init !== undefined) await adapter.init(this)
404
+ if (adapter.dispose !== undefined) {
405
+ this.disposeCallbacks.push(adapter.dispose.bind(adapter))
406
+ }
407
+ }
408
+
409
+ if (this.server !== undefined) {
410
+ for (const action of this.actions.values()) this.server.mount(action)
411
+ }
412
+
413
+ if (this.eventBus !== undefined) {
414
+ for (const reaction of this.reactions.values()) this.subscribeReaction(reaction)
415
+ } else if (this.reactions.size > 0) {
416
+ throw error({
417
+ code: 'runtime.eventbus_required',
418
+ category: 'internal',
419
+ message: `${this.reactions.size} reaction(s) registered without an EventBusAdapter`,
420
+ })
421
+ }
422
+
423
+ if (this.scheduler !== undefined) {
424
+ for (const schedule of this.schedules.values()) this.registerSchedule(schedule)
425
+ } else if (this.schedules.size > 0) {
426
+ throw error({
427
+ code: 'runtime.scheduler_required',
428
+ category: 'internal',
429
+ message: `${this.schedules.size} schedule(s) registered without a SchedulerAdapter`,
430
+ })
431
+ }
432
+
433
+ this.started = true
434
+ this.log.info('runtime started', {
435
+ actions: this.actions.size,
436
+ reactions: this.reactions.size,
437
+ schedules: this.schedules.size,
438
+ })
439
+ }
440
+
441
+ /**
442
+ * Graceful shutdown. Chama `dispose()` em cada adapter registrado.
443
+ */
444
+ async dispose(): Promise<void> {
445
+ this.log.info('runtime disposing')
446
+ const callbacks = this.disposeCallbacks.slice().reverse()
447
+ for (const cb of callbacks) {
448
+ try {
449
+ await cb()
450
+ } catch (err) {
451
+ this.log.warn('adapter dispose failed', { error: String(err) })
452
+ }
453
+ }
454
+ this.started = false
455
+ }
456
+
457
+ /**
458
+ * Agrega `healthCheck()` de todos os adapters registrados. Adapter sem
459
+ * `healthCheck` é considerado ok.
460
+ */
461
+ async healthCheck(): Promise<{
462
+ ok: boolean
463
+ adapters: Record<string, HealthStatus>
464
+ }> {
465
+ const adapters: Record<string, HealthStatus> = {}
466
+ let ok = true
467
+
468
+ for (const adapter of this.allAdapters()) {
469
+ const key = `${adapter.kind}:${adapter.name}`
470
+ if (adapter.healthCheck === undefined) {
471
+ adapters[key] = { ok: true }
472
+ continue
473
+ }
474
+ try {
475
+ const status = await adapter.healthCheck()
476
+ adapters[key] = status
477
+ if (!status.ok) ok = false
478
+ } catch (err) {
479
+ adapters[key] = { ok: false, details: { error: String(err) } }
480
+ ok = false
481
+ }
482
+ }
483
+
484
+ const auditHealth = await this.audit.healthCheck()
485
+ adapters['audit:emitter'] = { ok: auditHealth.ok, details: auditHealth.details }
486
+ if (!auditHealth.ok) ok = false
487
+
488
+ return { ok, adapters }
489
+ }
490
+
491
+ /**
492
+ * Executa uma action de forma síncrona. Background actions usam
493
+ * `executeBackground` (não implementado em v0; queue adapter dispara).
494
+ */
495
+ async execute(
496
+ actionName: string,
497
+ input: unknown,
498
+ ctxBase: ContextBase,
499
+ ): Promise<ActionResult<unknown>> {
500
+ const action = this.actions.get(actionName)
501
+ if (action === undefined) {
502
+ return this.errorResult(actionName, error({
503
+ code: 'runtime.action_not_found',
504
+ category: 'not_found',
505
+ message: `Action "${actionName}" is not registered`,
506
+ }), 0, ctxBase.requestId)
507
+ }
508
+
509
+ const startedAt = performance.now()
510
+ const actionId = generateId()
511
+ const ctx = this.buildActionContext(action, actionId, ctxBase)
512
+
513
+ try {
514
+ // — 1. Validate input ————————————————————————————————————————————————
515
+ const validatedInput = await this.validate(action.input, input, 'input')
516
+
517
+ // — 2. Load —————————————————————————————————————————————————————————
518
+ const loaded = await this.runLoaders(action, ctx, validatedInput)
519
+
520
+ // — 3. Authenticate (public-or-user) ——————————————————————————————————
521
+ if (action.public !== true && ctx.user === null) {
522
+ throw error({
523
+ code: 'auth.unauthenticated',
524
+ category: 'authentication',
525
+ message: 'Authentication required',
526
+ })
527
+ }
528
+
529
+ // — 4. Authorize ——————————————————————————————————————————————————————
530
+ if (action.authorize !== undefined) {
531
+ const authorizeFn = this.compileAuthorize(action.authorize)
532
+ const decision = await authorizeFn(ctx, validatedInput, loaded)
533
+ if (decision === false) {
534
+ throw error({
535
+ code: 'auth.forbidden',
536
+ category: 'authorization',
537
+ message: 'Forbidden',
538
+ })
539
+ }
540
+ if (decision !== true) throw decision
541
+ }
542
+
543
+ // — 5. Handler ————————————————————————————————————————————————————————
544
+ // Modo design (`OPUS_MODE=design`): tenta mockHandler primeiro, cai
545
+ // no handler real se não houver. Em qualquer outro modo, handler real
546
+ // sempre manda. Leitura de env mora aqui, no boundary do execute — o
547
+ // resto do core continua sem tocar `process.env`.
548
+ const isDesignMode = isOpusDesignMode()
549
+ const handlerToRun =
550
+ isDesignMode && action.mockHandler !== undefined
551
+ ? action.mockHandler
552
+ : action.handler
553
+ const output = await handlerToRun(ctx, validatedInput, loaded)
554
+ const usedMock = handlerToRun === action.mockHandler
555
+
556
+ // — 6. Validate output (dev only) —————————————————————————————————————
557
+ // Search retorna Paginated<Item> mas `output` schema descreve só Item;
558
+ // skipamos pra evitar falso negativo. v1 pode validar items[].
559
+ const skipOutputValidation = action.kind === 'list'
560
+ const validatedOutput =
561
+ this.config.validateOutputInDev && !skipOutputValidation
562
+ ? await this.validate(action.output, output, 'output')
563
+ : output
564
+
565
+ // — 7. Audit ———————————————————————————————————————————————————————————
566
+ const durationMs = performance.now() - startedAt
567
+ await this.audit.emit(
568
+ this.buildAuditRecord({
569
+ actionId,
570
+ action: action.name,
571
+ actionKind: action.kind,
572
+ outcome: 'success',
573
+ durationMs,
574
+ ctx,
575
+ input: validatedInput,
576
+ output: validatedOutput,
577
+ mode: usedMock ? 'design' : undefined,
578
+ }),
579
+ action.audit,
580
+ )
581
+
582
+ // — 8. Build result ———————————————————————————————————————————————————
583
+ return {
584
+ ok: true,
585
+ data: validatedOutput,
586
+ meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId),
587
+ }
588
+ } catch (thrown) {
589
+ const actionError = normalizeError(thrown)
590
+ const durationMs = performance.now() - startedAt
591
+ await this.audit.emit(
592
+ this.buildAuditRecord({
593
+ actionId,
594
+ action: action.name,
595
+ actionKind: action.kind,
596
+ outcome: 'error',
597
+ durationMs,
598
+ ctx,
599
+ input,
600
+ error: actionError,
601
+ }),
602
+ action.audit,
603
+ )
604
+ return {
605
+ ok: false,
606
+ error: actionError,
607
+ meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId),
608
+ }
609
+ }
610
+ }
611
+
612
+ // ===========================================================================
613
+ // Privados
614
+ // ===========================================================================
615
+
616
+ private *allAdapters(): IterableIterator<Adapter> {
617
+ if (this.logger !== undefined) yield this.logger
618
+ if (this.data !== undefined) yield this.data
619
+ if (this.auth !== undefined) yield this.auth
620
+ if (this.server !== undefined) yield this.server
621
+ if (this.queue !== undefined) yield this.queue
622
+ if (this.eventBus !== undefined) yield this.eventBus
623
+ if (this.scheduler !== undefined) yield this.scheduler
624
+ if (this.client !== undefined) yield this.client
625
+ if (this.storage !== undefined) yield this.storage
626
+ if (this.ai !== undefined) yield this.ai
627
+ }
628
+
629
+ /** As actions `ai:enabled` viradas em tool specs (cacheado — registry é estático pós-start).
630
+ * Cada tool carrega o Schema do input; quem consome (driver, MCP server) converte pra
631
+ * JSON Schema — o core não depende de zod-to-json-schema. Público: o bridge é reusado
632
+ * pela cola do ctx.ai E pelo MCP server (`@softize/opus/mcp`). */
633
+ aiTools(): AiTool[] {
634
+ if (this.aiToolsCache !== null) return this.aiToolsCache
635
+ const tools: AiTool[] = []
636
+ for (const action of this.actions.values()) {
637
+ const cfg = normalizeAiConfig(action.ai)
638
+ if (cfg === null) continue
639
+ tools.push({
640
+ name: action.name,
641
+ description: cfg.description ?? action.description ?? action.name,
642
+ inputSchema: action.input,
643
+ })
644
+ }
645
+ this.aiToolsCache = tools
646
+ return tools
647
+ }
648
+
649
+ /** Liga o adapter de IA a um contexto: o `run` sai com as tools das actions `ai:enabled`
650
+ * e o `execute` que roda a action COM ESTE contexto (auth de quem chamou) + o gate de
651
+ * confirmação (destructive/requiresConfirmation). `complete`/`extract` passam direto.
652
+ * Null se não há adapter. É a "cola" que mantém o driver puro (DI de tools+execute). */
653
+ private bindAi(base: ContextBase): BoundAi | null {
654
+ const adapter = this.ai
655
+ if (adapter === undefined) return null
656
+ const tools = this.aiTools()
657
+ // O execute-como-usuário + gate de confirmação é o mesmo nos dois modos (run/runStream).
658
+ const runOptions = (opts?: BoundAiRunOptions): AiRunOptions => ({
659
+ ...opts,
660
+ tools,
661
+ execute: async (name, toolInput) => {
662
+ const cfg = normalizeAiConfig(this.actions.get(name)?.ai)
663
+ if (cfg !== null && (cfg.destructive === true || cfg.requiresConfirmation === true)) {
664
+ const approved = opts?.confirm ? await opts.confirm({ name, input: toolInput }) : false
665
+ if (!approved) {
666
+ return { error: `A action "${name}" é destrutiva ou precisa de confirmação, e não foi aprovada.` }
667
+ }
668
+ }
669
+ const result = await this.execute(name, toolInput, base)
670
+ return result.ok ? result.data : { error: result.error }
671
+ },
672
+ })
673
+ return {
674
+ complete: (p, o) => adapter.complete(p, o),
675
+ extract: (p, s, o) => adapter.extract(p, s, o),
676
+ run: (input, opts) => {
677
+ if (adapter.run === undefined) {
678
+ throw new Error('O driver de IA configurado não implementa run (loop agêntico).')
679
+ }
680
+ return adapter.run(input, runOptions(opts))
681
+ },
682
+ runStream: (input, opts) => {
683
+ if (adapter.runStream === undefined) {
684
+ throw new Error('O driver de IA configurado não implementa runStream (loop agêntico em streaming).')
685
+ }
686
+ return adapter.runStream(input, runOptions(opts))
687
+ },
688
+ }
689
+ }
690
+
691
+ /** O adapter de IA LIGADO a um contexto — pra fora do handler (ex.: o backend do chat
692
+ * resolve o usuário e chama `runtime.aiFor(base).run(historico)`). Null sem adapter. */
693
+ aiFor(base: ContextBase): BoundAi | null {
694
+ return this.bindAi(base)
695
+ }
696
+
697
+ private buildActionContext(
698
+ action: ActionDef,
699
+ actionId: string,
700
+ base: ContextBase,
701
+ ): ActionContext {
702
+ const dbExt = this.data?.contextExtension() ?? { db: undefined }
703
+ const provenance = resolveProvenance(base)
704
+ const log = this.log.child({
705
+ action: action.name,
706
+ actionId,
707
+ provenance: provenance.kind,
708
+ ...(base.tenantId !== null ? { tenant: base.tenantId } : {}),
709
+ ...(base.user !== null ? { userId: base.user.id } : {}),
710
+ ...(base.requestId !== undefined ? { requestId: base.requestId } : {}),
711
+ })
712
+
713
+ return {
714
+ user: base.user,
715
+ tenantId: base.tenantId,
716
+ can: base.can,
717
+ db: dbExt.db,
718
+ log,
719
+ emit: this.buildEmit(action, actionId, base),
720
+ storage: this.storage ?? null,
721
+ ai: this.bindAi(base),
722
+ provenance,
723
+ meta: base.meta ?? {},
724
+ }
725
+ }
726
+
727
+ private buildEmit(
728
+ action: ActionDef,
729
+ actionId: string,
730
+ base: ContextBase,
731
+ ): ActionContext['emit'] {
732
+ return async (event, data, meta) => {
733
+ if (
734
+ this.config.warnUndeclaredEmits &&
735
+ actionHasEmits(action) &&
736
+ !action.emits?.includes(event)
737
+ ) {
738
+ this.log.warn('handler emitted undeclared event', {
739
+ action: action.name,
740
+ event,
741
+ declared: action.emits,
742
+ })
743
+ }
744
+
745
+ if (this.eventBus === undefined) {
746
+ if (this.config.emitMode === 'strict') {
747
+ throw error({
748
+ code: 'emit.no_bus',
749
+ category: 'internal',
750
+ message: `ctx.emit('${event}') called without EventBusAdapter`,
751
+ })
752
+ }
753
+ this.log.warn('emit dropped — no EventBusAdapter', { event })
754
+ return
755
+ }
756
+
757
+ const domainEvent: DomainEvent = {
758
+ type: event,
759
+ id: generateId(),
760
+ timestamp: new Date().toISOString(),
761
+ actor: {
762
+ id: base.user?.id ?? null,
763
+ type: base.user !== null ? 'user' : 'system',
764
+ },
765
+ tenant: base.tenantId,
766
+ data,
767
+ source: {
768
+ action: action.name,
769
+ actionId,
770
+ ...(meta?.correlation !== undefined ? { correlation: meta.correlation } : {}),
771
+ },
772
+ ...(meta !== undefined ? { meta: meta as Record<string, unknown> } : {}),
773
+ }
774
+
775
+ try {
776
+ await this.eventBus.publish(domainEvent)
777
+ } catch (err) {
778
+ if (this.config.emitMode === 'strict') {
779
+ throw error({
780
+ code: 'emit.failed',
781
+ category: 'internal',
782
+ message: `failed to publish event "${event}"`,
783
+ cause: String(err),
784
+ })
785
+ }
786
+ this.log.warn('event publish failed (lenient)', {
787
+ event,
788
+ error: String(err),
789
+ })
790
+ }
791
+ }
792
+ }
793
+
794
+ private async validate<T>(
795
+ schema: Schema<T> | Schema<unknown, T>,
796
+ value: unknown,
797
+ kind: 'input' | 'output',
798
+ ): Promise<T> {
799
+ const result = await Promise.resolve(schema['~standard'].validate(value))
800
+ if ('value' in result) return result.value as T
801
+ throw error({
802
+ code: kind === 'input' ? 'validation.invalid_input' : 'validation.invalid_output',
803
+ category: kind === 'input' ? 'validation' : 'internal',
804
+ message: kind === 'input' ? 'Input validation failed' : 'Output validation failed (dev)',
805
+ issues: result.issues.map((iss) => ({
806
+ path: (iss.path ?? []).join('.'),
807
+ code: 'invalid',
808
+ message: iss.message,
809
+ })),
810
+ })
811
+ }
812
+
813
+ private async runLoaders(
814
+ action: ActionDef,
815
+ ctx: ActionContext,
816
+ input: unknown,
817
+ ): Promise<Record<string, unknown> | undefined> {
818
+ const spec = action.loads as LoadsSpec<unknown> | undefined
819
+ if (spec === undefined) return undefined
820
+
821
+ const entries = this.normalizeLoadsEntries(spec)
822
+ const loaded: Record<string, unknown> = {}
823
+ for (const [key, loader] of entries) {
824
+ const value = await this.runSingleLoader(action, ctx, input, loader)
825
+ if (value === null || value === undefined) {
826
+ throw error({
827
+ code: `${action.name}.${key}.not_found`,
828
+ category: 'not_found',
829
+ message: `Resource "${key}" not found for action "${action.name}"`,
830
+ })
831
+ }
832
+ loaded[key] = value
833
+ }
834
+ return loaded
835
+ }
836
+
837
+ /**
838
+ * Normaliza `LoadsSpec` em entries `[key, spec]`. Aceita:
839
+ * - `Record<string, string | LoaderFn>` — key vira o key direto.
840
+ * - `string[]` — extrai entidade de cada DSL e usa como key.
841
+ */
842
+ private normalizeLoadsEntries(
843
+ spec: LoadsSpec<unknown>,
844
+ ): Array<[string, string | LoaderFn<unknown>]> {
845
+ if (Array.isArray(spec)) {
846
+ return spec.map((dsl) => {
847
+ const parsed = this.parseLoadCached(dsl)
848
+ return [parsed.entity, dsl] as [string, string]
849
+ })
850
+ }
851
+ return Object.entries(spec) as Array<[string, string | LoaderFn<unknown>]>
852
+ }
853
+
854
+ private async runSingleLoader(
855
+ action: ActionDef,
856
+ ctx: ActionContext,
857
+ input: unknown,
858
+ loader: string | LoaderFn<unknown>,
859
+ ): Promise<unknown> {
860
+ if (typeof loader === 'string') {
861
+ const parsed = this.parseLoadCached(loader)
862
+ const resolver = this.loaderResolvers.get(parsed.entity)
863
+ if (resolver === undefined) {
864
+ throw error({
865
+ code: 'runtime.loader_not_registered',
866
+ category: 'internal',
867
+ message: `Load spec "${loader}" em action "${action.name}" referencia entidade "${parsed.entity}" sem resolver registrado — use runtime.registerLoaders({ ${parsed.entity}: ... })`,
868
+ })
869
+ }
870
+ const args = resolveLoadArgs(parsed, (input ?? {}) as Record<string, unknown>)
871
+ return await resolver(args, ctx)
872
+ }
873
+ // closure legacy: (ctx, input)
874
+ return await loader(ctx, input)
875
+ }
876
+
877
+ private parseLoadCached(src: string): DslLoadSpec {
878
+ const cached = this.loadSpecCache.get(src)
879
+ if (cached !== undefined) return cached
880
+ const parsed = parseLoad(src)
881
+ this.loadSpecCache.set(src, parsed)
882
+ return parsed
883
+ }
884
+
885
+ /**
886
+ * Compila `AuthorizeSpec` (string DSL ou closure) numa `AuthorizeFn`.
887
+ * String é parseada uma vez (cache por instância de runtime); a função
888
+ * resultante avalia a expressão contra um contexto plano com:
889
+ * `{ user, input, ctx, ...loaded }`
890
+ * Resultado é coercido a boolean — DSL não retorna `ActionError`.
891
+ */
892
+ private compileAuthorize<In>(spec: AuthorizeSpec<In>): AuthorizeFn<In> {
893
+ if (typeof spec !== 'string') return spec
894
+ let ast = this.authorizeAstCache.get(spec)
895
+ if (ast === undefined) {
896
+ ast = parseExpression(spec)
897
+ this.authorizeAstCache.set(spec, ast)
898
+ }
899
+ const astCompiled = ast
900
+ return (ctx, input, loaded) => {
901
+ const evalCtx: Record<string, unknown> = {
902
+ user: ctx.user,
903
+ input,
904
+ ctx,
905
+ ...(loaded ?? {}),
906
+ }
907
+ return Boolean(evalExpression(astCompiled, evalCtx))
908
+ }
909
+ }
910
+
911
+ private buildAuditRecord(args: {
912
+ actionId: string
913
+ action: string
914
+ actionKind?: ActionDef['kind']
915
+ outcome: 'success' | 'error'
916
+ durationMs: number
917
+ ctx: ActionContext
918
+ input: unknown
919
+ output?: unknown
920
+ error?: ReturnType<typeof error>
921
+ /** Quando `'design'`, sinaliza que o mockHandler foi executado. */
922
+ mode?: 'design' | undefined
923
+ }): AuditRecord {
924
+ const record: AuditRecord = {
925
+ id: args.actionId,
926
+ timestamp: new Date().toISOString(),
927
+ action: args.action,
928
+ ...(args.actionKind !== undefined ? { actionKind: args.actionKind } : {}),
929
+ outcome: args.outcome,
930
+ durationMs: args.durationMs,
931
+ provenance: args.ctx.provenance,
932
+ actor: actorFromProvenance(args.ctx.provenance, args.ctx.user),
933
+ tenant: args.ctx.tenantId,
934
+ input: args.input,
935
+ severity: args.outcome === 'success' ? 'info' : severityForError(args.error),
936
+ }
937
+ if (args.output !== undefined) record.output = args.output
938
+ if (args.error !== undefined) record.error = args.error
939
+ if (args.mode !== undefined) record.meta = { mode: args.mode }
940
+ return record
941
+ }
942
+
943
+ private buildMeta(
944
+ actionId: string,
945
+ actionName: string,
946
+ durationMs: number,
947
+ requestId?: string,
948
+ ): ResultMeta {
949
+ const meta: ResultMeta = {
950
+ actionId,
951
+ action: actionName,
952
+ durationMs,
953
+ }
954
+ if (requestId !== undefined) meta.requestId = requestId
955
+ return meta
956
+ }
957
+
958
+ private errorResult(
959
+ actionName: string,
960
+ err: ReturnType<typeof error>,
961
+ durationMs: number,
962
+ requestId?: string,
963
+ ): ActionResult<never> {
964
+ return {
965
+ ok: false,
966
+ error: err,
967
+ meta: this.buildMeta(generateId(), actionName, durationMs, requestId),
968
+ }
969
+ }
970
+
971
+ private subscribeReaction(reaction: ReactionDef<any>): void {
972
+ if (this.eventBus?.subscribe === undefined) {
973
+ throw error({
974
+ code: 'runtime.subscribe_unsupported',
975
+ category: 'internal',
976
+ message: 'EventBusAdapter does not support subscribe()',
977
+ })
978
+ }
979
+ const patterns = Array.isArray(reaction.on) ? reaction.on : [reaction.on]
980
+ for (const pattern of patterns) {
981
+ const unsub = this.eventBus.subscribe(pattern, (event) => {
982
+ void this.runReaction(reaction, event)
983
+ })
984
+ this.disposeCallbacks.push(() => unsub())
985
+ }
986
+ }
987
+
988
+ private registerSchedule(schedule: ScheduleDef): void {
989
+ if (schedule.enabled === false) {
990
+ this.log.info('schedule disabled', { schedule: schedule.name })
991
+ return
992
+ }
993
+ // start() já validou que this.scheduler existe quando há schedules.
994
+ const scheduler = this.scheduler as SchedulerAdapter
995
+ const unsub = scheduler.register(schedule, async () => {
996
+ await this.fireSchedule(schedule)
997
+ })
998
+ this.disposeCallbacks.push(() => unsub())
999
+ }
1000
+
1001
+ private async fireSchedule(schedule: ScheduleDef): Promise<void> {
1002
+ const input =
1003
+ typeof schedule.input === 'function'
1004
+ ? await (schedule.input as () => unknown | Promise<unknown>)()
1005
+ : (schedule.input ?? {})
1006
+ await this.execute(schedule.action, input, {
1007
+ user: null,
1008
+ tenantId: null,
1009
+ can: () => false,
1010
+ provenance: { kind: 'schedule', scheduleName: schedule.name },
1011
+ })
1012
+ }
1013
+
1014
+ private async runReaction(
1015
+ reaction: ReactionDef<any>,
1016
+ event: DomainEvent,
1017
+ ): Promise<void> {
1018
+ const provenance: Provenance = {
1019
+ kind: 'reaction',
1020
+ reactionName: reaction.name,
1021
+ sourceActionId: event.source.actionId,
1022
+ eventType: event.type,
1023
+ }
1024
+ const ctx: ReactionContext = {
1025
+ user: event.actor.id !== null ? ({ id: event.actor.id } as User) : null,
1026
+ tenantId: event.tenant ?? null,
1027
+ db: this.data?.contextExtension().db,
1028
+ log: this.log.child({
1029
+ reaction: reaction.name,
1030
+ eventType: event.type,
1031
+ provenance: 'reaction',
1032
+ }),
1033
+ emit: noopEmit,
1034
+ storage: this.storage ?? null,
1035
+ // Reação = contexto de sistema; o agente só alcança actions públicas (can nega por
1036
+ // padrão — nada de LLM disparado por evento chamando action gated sem gate explícito).
1037
+ ai: this.bindAi({
1038
+ user: event.actor.id !== null ? ({ id: event.actor.id } as User) : null,
1039
+ tenantId: event.tenant ?? null,
1040
+ can: async () => false,
1041
+ meta: {},
1042
+ }),
1043
+ provenance,
1044
+ meta: {},
1045
+ }
1046
+
1047
+ try {
1048
+ if (reaction.authorize !== undefined) {
1049
+ const allowed = await reaction.authorize(ctx, event)
1050
+ if (!allowed) return
1051
+ }
1052
+ await reaction.handler(ctx, event)
1053
+ } catch (err) {
1054
+ const actionError = isActionError(err) ? err : normalizeError(err)
1055
+ ctx.log.error('reaction failed', {
1056
+ code: actionError.code,
1057
+ message: actionError.message,
1058
+ })
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ // =============================================================================
1064
+ // Helpers
1065
+ // =============================================================================
1066
+
1067
+ // ContextBase moveu pra types.ts (compartilhado entre core e adapters).
1068
+
1069
+ /**
1070
+ * Resolve provenance do ContextBase, com default `{ kind: 'system', source: 'unknown' }`
1071
+ * quando caller não passa. Server adapter / scheduler / runtime devem sempre
1072
+ * passar provenance explícita; default é safety net.
1073
+ */
1074
+ function resolveProvenance(base: ContextBase): Provenance {
1075
+ if (base.provenance !== undefined) return base.provenance
1076
+ return { kind: 'system', source: 'unknown' }
1077
+ }
1078
+
1079
+ /**
1080
+ * Deriva o shape legado `actor: { id, type }` a partir de Provenance,
1081
+ * preservando retrocompat de AuditRecord.
1082
+ */
1083
+ function actorFromProvenance(
1084
+ provenance: Provenance,
1085
+ user: User | null,
1086
+ ): AuditRecord['actor'] {
1087
+ const base = ((): AuditRecord['actor'] => {
1088
+ switch (provenance.kind) {
1089
+ case 'http':
1090
+ return user !== null
1091
+ ? { id: user.id, type: 'user' }
1092
+ : { id: null, type: 'user' }
1093
+ case 'integration':
1094
+ return { id: provenance.name, type: 'integration' }
1095
+ case 'ai-agent':
1096
+ return { id: provenance.agentId, type: 'integration' }
1097
+ case 'schedule':
1098
+ case 'reaction':
1099
+ case 'background':
1100
+ case 'self-observation':
1101
+ case 'system':
1102
+ return { id: user?.id ?? null, type: 'system' }
1103
+ }
1104
+ })()
1105
+ const meta = userMeta(user)
1106
+ return meta === undefined ? base : { ...base, meta }
1107
+ }
1108
+
1109
+ /**
1110
+ * name/email do user resolvido → `actor.meta` — o que todo sink re-resolvia
1111
+ * (id→nome) por fora. Só os dois campos de identificação: o User é shape
1112
+ * aberto e despejá-lo inteiro no log vazaria o que ninguém pediu.
1113
+ */
1114
+ function userMeta(user: User | null): Record<string, unknown> | undefined {
1115
+ if (user === null) return undefined
1116
+ const meta: Record<string, unknown> = {}
1117
+ if (typeof user['name'] === 'string') meta['name'] = user['name']
1118
+ if (typeof user['email'] === 'string') meta['email'] = user['email']
1119
+ return Object.keys(meta).length > 0 ? meta : undefined
1120
+ }
1121
+
1122
+ function actionHasEmits(action: ActionDef): action is ActionDef & { emits: string[] } {
1123
+ return (
1124
+ (action.kind === 'simple' || action.kind === 'form') &&
1125
+ Array.isArray((action as any).emits)
1126
+ )
1127
+ }
1128
+
1129
+ function severityForError(err: any): 'info' | 'warning' | 'error' {
1130
+ /* v8 ignore next — defensivo; buildAuditRecord só chama com err presente */
1131
+ if (err === undefined) return 'info'
1132
+ if (err.severity === 'fatal') return 'error'
1133
+ if (err.severity === 'warning') return 'warning'
1134
+ return 'error'
1135
+ }
1136
+
1137
+ /**
1138
+ * Placeholder pra `ctx.emit` em reactions. Reaction chain (reaction emite
1139
+ * eventos novos que disparam outras reactions) entra em v1+.
1140
+ */
1141
+ const noopEmit: ActionContext['emit'] = async () => undefined
1142
+
1143
+ function generateId(): string {
1144
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
1145
+ return crypto.randomUUID()
1146
+ }
1147
+ // fallback determinístico-suficiente quando crypto não disponível
1148
+ return `id_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`
1149
+ }
1150
+
1151
+ /**
1152
+ * Único ponto do core que lê `process.env`. Usado pelo `execute()` pra
1153
+ * decidir entre `handler` real e `mockHandler`. Isolado em função pra
1154
+ * testabilidade — testes podem `vi.stubEnv('OPUS_MODE', ...)`.
1155
+ *
1156
+ * Mantém a invariante "core nunca lê env" intacta no resto do código:
1157
+ * adapters/config seguem recebendo valores literais.
1158
+ *
1159
+ * Exportado (fora do index público) pro harness de `testing` aplicar a MESMA
1160
+ * régua no `runAction` — a semântica de design mora num lugar só.
1161
+ */
1162
+ export function isOpusDesignMode(): boolean {
1163
+ // Acesso via globalThis: defensivo pra runtimes sem `process` (browser, edge) E
1164
+ // não exige @types/node no consumidor que compila este source (apps web).
1165
+ const g = globalThis as { process?: { env?: Record<string, string | undefined> } }
1166
+ return g.process?.env?.['OPUS_MODE'] === 'design'
1167
+ }