@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,239 @@
1
+ /**
2
+ * tbdlib — AuditEmitter
3
+ *
4
+ * Classe que coordena a emissão de `AuditRecord` pros `AuditSink` registrados.
5
+ * Aplica config da action (redact, fields, severity, sink filter) antes de
6
+ * dispatchar; trata falhas conforme `auditMode` ('lenient' | 'strict').
7
+ *
8
+ * Ver §5 do protocolo.
9
+ */
10
+
11
+ import { error } from './errors.ts'
12
+ import type {
13
+ AuditConfig,
14
+ AuditRecord,
15
+ AuditSink,
16
+ Logger,
17
+ } from './types.ts'
18
+
19
+ interface AuditEmitterOptions {
20
+ mode: 'lenient' | 'strict'
21
+ log: Logger
22
+ }
23
+
24
+ export class AuditEmitter {
25
+ private readonly sinks: AuditSink[] = []
26
+ private readonly mode: 'lenient' | 'strict'
27
+ private readonly log: Logger
28
+
29
+ constructor(options: AuditEmitterOptions) {
30
+ this.mode = options.mode
31
+ this.log = options.log
32
+ }
33
+
34
+ /**
35
+ * Registra um sink. Sinks são emitidos em paralelo; ordem de registro
36
+ * não importa.
37
+ */
38
+ register(sink: AuditSink): void {
39
+ this.sinks.push(sink)
40
+ }
41
+
42
+ /**
43
+ * Emite um record pros sinks apropriados.
44
+ * - `config === false` → no-op (audit desabilitado pra esta action).
45
+ * - `config === true | undefined` → record bruto, todos os sinks.
46
+ * - `config: AuditConfig` → aplica redact/fields/severity/sink filter.
47
+ *
48
+ * Falhas seguem o `auditMode`:
49
+ * - `lenient` (default): warn no logger, processo segue.
50
+ * - `strict`: lança ActionError de categoria 'internal', aborta o flow.
51
+ */
52
+ async emit(
53
+ record: AuditRecord,
54
+ config?: boolean | AuditConfig,
55
+ ): Promise<void> {
56
+ if (config === false) return
57
+
58
+ const processed = this.applyConfig(record, config)
59
+ const targetSinks = this.filterSinks(config)
60
+
61
+ if (targetSinks.length === 0) return
62
+
63
+ const results = await Promise.allSettled(
64
+ targetSinks.map((sink) => Promise.resolve(sink.emit(processed))),
65
+ )
66
+
67
+ const failures = results.filter(
68
+ (r): r is PromiseRejectedResult => r.status === 'rejected',
69
+ )
70
+
71
+ if (failures.length === 0) return
72
+
73
+ if (this.mode === 'strict') {
74
+ throw error({
75
+ code: 'audit.sink.failed',
76
+ category: 'internal',
77
+ message: `${failures.length}/${targetSinks.length} audit sinks failed`,
78
+ meta: { failures: failures.map((f) => String(f.reason)) },
79
+ })
80
+ }
81
+
82
+ this.log.warn('audit sinks failed (lenient mode)', {
83
+ failures: failures.length,
84
+ total: targetSinks.length,
85
+ reasons: failures.map((f) => String(f.reason)),
86
+ })
87
+ }
88
+
89
+ /**
90
+ * Health check: ok se todos os sinks declararam ok (ou nenhum implementou
91
+ * healthCheck). Detalhes do estado de cada sink no `details`.
92
+ */
93
+ async healthCheck(): Promise<{ ok: boolean; details: Record<string, unknown> }> {
94
+ const details: Record<string, unknown> = {}
95
+ let ok = true
96
+
97
+ for (const sink of this.sinks) {
98
+ if (sink.healthCheck === undefined) {
99
+ details[sink.name] = { ok: true, skipped: true }
100
+ continue
101
+ }
102
+ try {
103
+ const result = await sink.healthCheck()
104
+ details[sink.name] = result
105
+ if (!result.ok) ok = false
106
+ } catch (err) {
107
+ details[sink.name] = { ok: false, error: String(err) }
108
+ ok = false
109
+ }
110
+ }
111
+
112
+ return { ok, details }
113
+ }
114
+
115
+ // ===========================================================================
116
+ // Internos
117
+ // ===========================================================================
118
+
119
+ private applyConfig(
120
+ record: AuditRecord,
121
+ config: boolean | AuditConfig | undefined,
122
+ ): AuditRecord {
123
+ if (config === undefined || config === true) return record
124
+ /* v8 ignore next 2 — defensivo; emit() já short-circuita config=false */
125
+ if (config === false) return record
126
+
127
+ let input = record.input
128
+ if (config.fields !== undefined) input = pickFields(input, config.fields)
129
+ if (config.redact !== undefined) input = redactPaths(input, config.redact)
130
+
131
+ let output: unknown = record.output
132
+ let suppressOutput = false
133
+ if (config.output === false) {
134
+ suppressOutput = true
135
+ } else if (typeof config.output === 'object' && record.output !== undefined) {
136
+ if (config.output.fields !== undefined)
137
+ output = pickFields(record.output, config.output.fields)
138
+ if (config.output.redact !== undefined)
139
+ output = redactPaths(output, config.output.redact)
140
+ }
141
+
142
+ const { output: _stripped, ...rest } = record
143
+ const next: AuditRecord = { ...rest, input }
144
+ if (!suppressOutput && output !== undefined) next.output = output
145
+ if (config.severity !== undefined) next.severity = config.severity
146
+
147
+ return next
148
+ }
149
+
150
+ private filterSinks(config: boolean | AuditConfig | undefined): AuditSink[] {
151
+ if (config === undefined || config === true || config === false) {
152
+ return this.sinks
153
+ }
154
+ if (config.sink === undefined) return this.sinks
155
+ return this.sinks.filter((sink) => sink.name === config.sink)
156
+ }
157
+ }
158
+
159
+ // =============================================================================
160
+ // Path helpers
161
+ // =============================================================================
162
+
163
+ /**
164
+ * Retorna um novo objeto contendo apenas as chaves listadas em `fields`.
165
+ * Suporta dot-paths simples ("payment.amount"); arrays não são percorridos.
166
+ */
167
+ function pickFields(value: unknown, fields: string[]): unknown {
168
+ if (!isPlainObject(value)) return value
169
+ const result: Record<string, unknown> = {}
170
+ for (const field of fields) {
171
+ const found = getAtPath(value, field)
172
+ if (found.exists) {
173
+ setAtPath(result, field, found.value)
174
+ }
175
+ }
176
+ return result
177
+ }
178
+
179
+ /**
180
+ * Substitui valores nos paths informados por '[REDACTED]'.
181
+ * Não mutaiona o original.
182
+ */
183
+ function redactPaths(value: unknown, paths: string[]): unknown {
184
+ if (!isPlainObject(value)) return value
185
+ const cloned = deepClone(value)
186
+ for (const path of paths) {
187
+ const found = getAtPath(cloned, path)
188
+ if (found.exists) setAtPath(cloned, path, '[REDACTED]')
189
+ }
190
+ return cloned
191
+ }
192
+
193
+ function getAtPath(
194
+ obj: unknown,
195
+ path: string,
196
+ ): { exists: boolean; value: unknown } {
197
+ const parts = path.split('.')
198
+ let cursor: unknown = obj
199
+ for (const part of parts) {
200
+ if (!isPlainObject(cursor) || !(part in cursor)) {
201
+ return { exists: false, value: undefined }
202
+ }
203
+ cursor = (cursor as Record<string, unknown>)[part]
204
+ }
205
+ return { exists: true, value: cursor }
206
+ }
207
+
208
+ function setAtPath(obj: Record<string, unknown>, path: string, value: unknown): void {
209
+ const parts = path.split('.')
210
+ let cursor: Record<string, unknown> = obj
211
+ for (let i = 0; i < parts.length - 1; i += 1) {
212
+ const part = parts[i] as string
213
+ const next = cursor[part]
214
+ if (!isPlainObject(next)) {
215
+ const fresh: Record<string, unknown> = {}
216
+ cursor[part] = fresh
217
+ cursor = fresh
218
+ } else {
219
+ cursor = next as Record<string, unknown>
220
+ }
221
+ }
222
+ cursor[parts[parts.length - 1] as string] = value
223
+ }
224
+
225
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
226
+ return (
227
+ typeof value === 'object' &&
228
+ value !== null &&
229
+ !Array.isArray(value) &&
230
+ Object.getPrototypeOf(value) === Object.prototype
231
+ )
232
+ }
233
+
234
+ function deepClone<T>(value: T): T {
235
+ if (typeof structuredClone === 'function') {
236
+ return structuredClone(value)
237
+ }
238
+ return JSON.parse(JSON.stringify(value)) as T
239
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @softize/opus/core — `defineContract` + `bindAction` (split contrato/handler).
3
+ *
4
+ * O `defineAction` junta o **contrato** (declarativo) com o **handler**
5
+ * (server-only) num objeto só — o que impede o frontend de importar a action
6
+ * sem arrastar db/segredos. `defineContract`/`bindAction` partem isso:
7
+ *
8
+ * - **Contrato** (roda nos dois lados → `shared`): identidade, input/output,
9
+ * fields/filters/projection, docs, e a regra `authorize` action-level.
10
+ * - **Binding** (server-only → `api`): `handler`, `loads`, `background`,
11
+ * `emits`, `idempotency`, e um `authorize` row-level que sobrescreve.
12
+ *
13
+ * O `bindAction(contract, binding)` produz um `ActionDef` normal — o runtime
14
+ * nem sabe que houve split. Ver §15 do protocolo / `docs/data-layer.md`.
15
+ */
16
+
17
+ import type {
18
+ ActionContext,
19
+ ActionDef,
20
+ AuthorizeSpec,
21
+ BackgroundConfig,
22
+ FormAction,
23
+ InferOutput,
24
+ LoadsSpec,
25
+ Paginated,
26
+ ProgressReporter,
27
+ ListAction,
28
+ SimpleAction,
29
+ ViewAction,
30
+ } from './types.ts'
31
+
32
+ /* eslint-disable @typescript-eslint/no-explicit-any */
33
+
34
+ // =============================================================================
35
+ // Contrato = ActionDef sem os campos de execução
36
+ // =============================================================================
37
+
38
+ /** Campos server-only que o `bindAction` fornece — o resto é contrato. */
39
+ type BindingKeys = 'handler' | 'loads' | 'background' | 'emits' | 'idempotency'
40
+
41
+ /**
42
+ * `ParsedIn` (o lado PARSEADO do schema de input — defaults aplicados) defaulta
43
+ * a `any` nos contratos, de propósito: quem anota contrato à mão é o CLIENTE
44
+ * (`FormContract<MeuInput, MeuOutput>` em hooks/props), que só conhece o fio —
45
+ * e `authorize`/`mockHandler` são contravariantes em `ParsedIn`, então um
46
+ * default estreito rejeitaria contrato real cujo schema tem `.default()`.
47
+ * Quem precisa do parseado com precisão (`bindAction`) o infere do schema.
48
+ */
49
+ export type SimpleContract<In = unknown, Out = unknown, ParsedIn = any> = Omit<
50
+ SimpleAction<In, Out, ParsedIn>,
51
+ BindingKeys
52
+ >
53
+ export type FormContract<
54
+ In extends Record<string, unknown> = Record<string, unknown>,
55
+ Out = unknown,
56
+ ParsedIn = any,
57
+ > = Omit<FormAction<In, Out, ParsedIn>, BindingKeys>
58
+ export type ListContract<In = unknown, Out = unknown, ParsedIn = any> = Omit<
59
+ ListAction<In, Out, ParsedIn>,
60
+ BindingKeys
61
+ >
62
+ export type ViewContract<In = unknown, Out = unknown, ParsedIn = any> = Omit<
63
+ ViewAction<In, Out, ParsedIn>,
64
+ BindingKeys
65
+ >
66
+
67
+ export type ActionContract<In = any, Out = any, ParsedIn = any> =
68
+ | SimpleContract<In, Out, ParsedIn>
69
+ | FormContract<In & Record<string, unknown>, Out, ParsedIn>
70
+ | ListContract<In, Out, ParsedIn>
71
+ | ViewContract<In, Out, ParsedIn>
72
+
73
+ /**
74
+ * Parte server-only amarrada por `bindAction`. `authorize` aqui (row-level)
75
+ * sobrescreve a do contrato. O retorno do handler aceita `Paginated<Out>` pra
76
+ * cobrir o kind `search` (cujo `output` descreve o item, não o envelope).
77
+ *
78
+ * Tudo aqui roda PÓS-validação — o runtime valida o input antes de executar —
79
+ * então o binding é tipado por `ParsedIn` (o output do schema de input,
80
+ * defaults aplicados), não pelo lado do fio.
81
+ */
82
+ export interface ActionBinding<ParsedIn, Out> {
83
+ handler: (
84
+ ctx: ActionContext,
85
+ input: ParsedIn,
86
+ loaded?: Record<string, unknown>,
87
+ progress?: ProgressReporter,
88
+ ) => Out | Paginated<Out> | Promise<Out | Paginated<Out>>
89
+ loads?: LoadsSpec<ParsedIn>
90
+ authorize?: AuthorizeSpec<ParsedIn>
91
+ background?: BackgroundConfig
92
+ emits?: string[]
93
+ idempotency?: (input: ParsedIn) => string
94
+ }
95
+
96
+ // =============================================================================
97
+ // defineContract (overloads por kind, igual defineAction)
98
+ // =============================================================================
99
+
100
+ export function defineContract<In, Out, ParsedIn = In>(
101
+ contract: SimpleContract<In, Out, ParsedIn>,
102
+ ): SimpleContract<In, Out, ParsedIn>
103
+ export function defineContract<In extends Record<string, unknown>, Out, ParsedIn = In>(
104
+ contract: FormContract<In, Out, ParsedIn>,
105
+ ): FormContract<In, Out, ParsedIn>
106
+ export function defineContract<In, Out, ParsedIn = In>(
107
+ contract: ListContract<In, Out, ParsedIn>,
108
+ ): ListContract<In, Out, ParsedIn>
109
+ export function defineContract<In, Out, ParsedIn = In>(
110
+ contract: ViewContract<In, Out, ParsedIn>,
111
+ ): ViewContract<In, Out, ParsedIn>
112
+ export function defineContract(contract: any): any {
113
+ return contract
114
+ }
115
+
116
+ // =============================================================================
117
+ // bindAction
118
+ // =============================================================================
119
+
120
+ /**
121
+ * Amarra o trabalho server-only a um contrato, produzindo um `ActionDef`.
122
+ * `binding.authorize` (row-level, com `loaded`) sobrescreve o `authorize`
123
+ * action-level do contrato.
124
+ */
125
+ // O input do binding é o PARSEADO: extraído do lado de output do schema de
126
+ // input do contrato (`InferOutput`), porque o runtime valida antes de executar.
127
+ type ContractParsedIn<C extends ActionContract> = InferOutput<C['input']>
128
+ type ContractOut<C> = C extends ActionContract<any, infer Out, any> ? Out : never
129
+
130
+ export function bindAction<C extends ActionContract>(
131
+ contract: C,
132
+ // ParsedIn/Out são extraídos do contrato (C concreto), nunca do handler —
133
+ // senão o retorno `Paginated<Out>` do search contaminaria a inferência de `Out`.
134
+ binding: ActionBinding<ContractParsedIn<C>, ContractOut<C>>,
135
+ ): ActionDef {
136
+ return { ...contract, ...binding } as unknown as ActionDef
137
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * tbdlib — `defineDomain` factory + flatten helper.
3
+ *
4
+ * Domain agrupa as peças que pertencem a um mesmo recorte funcional
5
+ * (dicts, models, repository, service, actions, reactions, schedules,
6
+ * subdomains). É puramente declarativo: nenhum efeito colateral acontece
7
+ * em `defineDomain`. O runtime consome o objeto via `flattenDomain` quando
8
+ * recebe um `DomainConfig` em `register(...)`.
9
+ *
10
+ * Validações em runtime (em `defineDomain`):
11
+ * - `name` é string não-vazia no formato `[a-z][a-zA-Z0-9_]*`.
12
+ * - Actions têm nomes únicos dentro do domínio (incluindo subdomains).
13
+ * - Subdomains têm nomes únicos entre siblings.
14
+ *
15
+ * Warnings (em `Runtime.register`, via `collectDomainWarnings`):
16
+ * - Actions cujo `name` não casa com `<root>(\.<sub>)*\.<verb>` emitem
17
+ * `console.warn`. Heurística que depende do path completo da raiz —
18
+ * por isso fica pro register, não pro `defineDomain` (que pode rodar
19
+ * standalone como subdomain).
20
+ *
21
+ * Ver §17 do protocolo (TBD em v2).
22
+ */
23
+
24
+ import type { ActionContract } from './contracts.ts'
25
+ import { error } from './errors.ts'
26
+ import type { ActionDef, ReactionDef, ScheduleDef } from './types.ts'
27
+
28
+ /* eslint-disable @typescript-eslint/no-explicit-any */
29
+
30
+ // =============================================================================
31
+ // Tipos
32
+ // =============================================================================
33
+
34
+ /**
35
+ * Um registrável de action num domínio: BOUND (`ActionDef`, com handler) ou
36
+ * contrato PURO (`ActionContract`, sem handler). Um domínio de design — a
37
+ * fatia isomorfa (ADR 0004) — registra só contratos, e em modo design o
38
+ * `mockHandler` responde. Fora de design, contrato sem handler falha no
39
+ * `execute` (comportamento INTENCIONAL); o tipo só deixa de exigir o cast.
40
+ */
41
+ export type DomainAction = ActionDef | ActionContract
42
+
43
+ /**
44
+ * Especificação declarativa de um domínio. Todos os campos exceto `name`
45
+ * são opcionais — domínio mínimo é só um `name`.
46
+ */
47
+ export interface DomainConfig {
48
+ /** Nome do domínio (ex: `tickets`, `shipments`). camelCase. */
49
+ name: string
50
+
51
+ /** Doc de negócio do domínio — o recorte que ele cobre. Flui pro manifest/lente. */
52
+ description?: string
53
+
54
+ /** Dicionários do domínio (ver `@softize/opus/schema`). Map de nome → instance. */
55
+ dicts?: Record<string, unknown>
56
+
57
+ /** Entidades do domínio (`defineEntity`). Map de nome → EntityConfig. É a fonte
58
+ * estrutural+negócio que vai pro manifest. `models` é o nome legado (alias). */
59
+ entities?: Record<string, unknown>
60
+
61
+ /** @deprecated Use `entities`. Mantido por compat — o runtime/gen lê os dois. */
62
+ models?: Record<string, unknown>
63
+
64
+ /** Repository class do domínio. */
65
+ repository?: unknown
66
+
67
+ /** Service class do domínio. */
68
+ service?: unknown
69
+
70
+ /**
71
+ * Actions do domínio — cada item é `ActionDef` (bound) ou `ActionContract`
72
+ * (contrato puro, sem handler; ver `DomainAction`). Pode ser:
73
+ * - `Record<string, DomainAction>` — map nome → action
74
+ * - `DomainAction[]` — array
75
+ * - `Record<string, DomainAction[]>` — barrel imports (`import * as actions`)
76
+ */
77
+ actions?: Record<string, DomainAction | DomainAction[]> | DomainAction[]
78
+
79
+ /** Reactions registradas no domínio. */
80
+ reactions?: Array<ReactionDef<any>>
81
+
82
+ /** Schedules registrados no domínio. */
83
+ schedules?: ScheduleDef[]
84
+
85
+ /** Subdomínios (recursivo). */
86
+ subdomains?: DomainConfig[]
87
+ }
88
+
89
+ /**
90
+ * Resultado de `flattenDomain` — arrays achatados prontos pra `Runtime.register`.
91
+ */
92
+ export interface FlattenedDomain {
93
+ actions: ActionDef[]
94
+ reactions: Array<ReactionDef<any>>
95
+ schedules: ScheduleDef[]
96
+ }
97
+
98
+ // =============================================================================
99
+ // defineDomain
100
+ // =============================================================================
101
+
102
+ const DOMAIN_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/
103
+
104
+ /**
105
+ * Constrói e valida um `DomainConfig`. Validação roda em runtime; em sucesso
106
+ * retorna o próprio config (identity, pra preservar inferência).
107
+ *
108
+ * Erros são `ActionError` com `category: 'internal'`.
109
+ *
110
+ * Warnings de nomenclatura (action que não casa com `<domain>(.sub)*.verb`)
111
+ * **não** são emitidos aqui — eles dependem do path completo da raiz e
112
+ * portanto saem em `Runtime.register([domain])` (ver `collectDomainWarnings`).
113
+ */
114
+ export function defineDomain(config: DomainConfig): DomainConfig {
115
+ validateDomain(config, [])
116
+ return config
117
+ }
118
+
119
+ /**
120
+ * Walk no domínio emitindo warnings (via `console.warn`) pra actions cujo
121
+ * `name` não casa com o path completo `<root>(.sub)*.verb`. Chamado pelo
122
+ * `Runtime.register` quando recebe um `DomainConfig`.
123
+ */
124
+ export function collectDomainWarnings(domain: DomainConfig): void {
125
+ walkForWarnings(domain, [])
126
+ }
127
+
128
+ /**
129
+ * Heurística simples pra detectar `DomainConfig` numa lista mista em
130
+ * `Runtime.register([...])`. Considera domain quando tem `name` (string)
131
+ * **sem** `kind` (descarta `ActionDef`) e **sem** `on`/`handler` no shape
132
+ * de reaction. Schedules têm `name + cron/every + action`, mas `action`
133
+ * é string — domain pode ter `actions` (plural). Faz a checagem por
134
+ * exclusão.
135
+ */
136
+ export function isDomainConfig(value: unknown): value is DomainConfig {
137
+ if (typeof value !== 'object' || value === null) return false
138
+ const v = value as Record<string, unknown>
139
+ if (typeof v.name !== 'string') return false
140
+ // ActionDef tem `kind` literal ('simple' | 'form' | 'list' | 'view').
141
+ if (typeof v.kind === 'string') return false
142
+ // ReactionDef tem `on` + `handler`.
143
+ if ('on' in v && 'handler' in v) return false
144
+ // ScheduleDef tem `action` (string) + `cron`/`every`.
145
+ if (typeof v.action === 'string') return false
146
+ // Domain precisa de pelo menos um dos campos abaixo pra ser útil — o
147
+ // teste impede falso positivo em `{ name: 'x' }` solto que sobrou de
148
+ // outra coisa.
149
+ return (
150
+ 'actions' in v ||
151
+ 'reactions' in v ||
152
+ 'schedules' in v ||
153
+ 'subdomains' in v ||
154
+ 'dicts' in v ||
155
+ 'models' in v ||
156
+ 'repository' in v ||
157
+ 'service' in v
158
+ )
159
+ }
160
+
161
+ /**
162
+ * Achata um domínio (e seus subdomains recursivamente) em arrays prontos
163
+ * pro `Runtime.register`.
164
+ */
165
+ export function flattenDomain(domain: DomainConfig): FlattenedDomain {
166
+ const out: FlattenedDomain = { actions: [], reactions: [], schedules: [] }
167
+ collect(domain, out)
168
+ return out
169
+ }
170
+
171
+ // =============================================================================
172
+ // Internos
173
+ // =============================================================================
174
+
175
+ function collect(domain: DomainConfig, out: FlattenedDomain): void {
176
+ for (const action of iterateActions(domain.actions)) {
177
+ out.actions.push(action)
178
+ }
179
+ if (domain.reactions !== undefined) {
180
+ for (const reaction of domain.reactions) out.reactions.push(reaction)
181
+ }
182
+ if (domain.schedules !== undefined) {
183
+ for (const schedule of domain.schedules) out.schedules.push(schedule)
184
+ }
185
+ if (domain.subdomains !== undefined) {
186
+ for (const sub of domain.subdomains) collect(sub, out)
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Normaliza `actions` (record | array | record-of-array) em iterável plano
192
+ * de `ActionDef`.
193
+ */
194
+ function* iterateActions(
195
+ actions: DomainConfig['actions'],
196
+ ): IterableIterator<ActionDef> {
197
+ if (actions === undefined) return
198
+ // Um contrato puro (`ActionContract`, sem handler) entra como `ActionDef` no
199
+ // runtime — em design o `mockHandler` responde; fora de design, a ausência de
200
+ // handler falha no `execute` (intencional). O cast reconcilia o tipo neste
201
+ // boundary de normalização; ver `DomainAction`.
202
+ if (Array.isArray(actions)) {
203
+ for (const a of actions) yield a as ActionDef
204
+ return
205
+ }
206
+ for (const value of Object.values(actions)) {
207
+ if (Array.isArray(value)) {
208
+ for (const a of value) yield a as ActionDef
209
+ } else {
210
+ yield value as ActionDef
211
+ }
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Valida um domínio recursivamente. `path` é a trilha dos ancestrais
217
+ * (camelCase concatenado em mensagens de erro).
218
+ */
219
+ function validateDomain(domain: DomainConfig, path: string[]): void {
220
+ // — name —
221
+ if (typeof domain.name !== 'string' || domain.name.length === 0) {
222
+ throw error({
223
+ code: 'domain.invalid_name',
224
+ category: 'internal',
225
+ message: `Domain "name" deve ser string não-vazia${path.length > 0 ? ` (em "${path.join('.')}")` : ''}`,
226
+ })
227
+ }
228
+ if (!DOMAIN_NAME_RE.test(domain.name)) {
229
+ throw error({
230
+ code: 'domain.invalid_name_format',
231
+ category: 'internal',
232
+ message: `Domain name "${domain.name}" deve casar com [a-z][a-zA-Z0-9_]*${path.length > 0 ? ` (em "${path.join('.')}")` : ''}`,
233
+ })
234
+ }
235
+
236
+ const fullPath = [...path, domain.name]
237
+ const fullPathStr = fullPath.join('.')
238
+
239
+ // — subdomains: nomes únicos entre siblings —
240
+ if (domain.subdomains !== undefined) {
241
+ const seen = new Set<string>()
242
+ for (const sub of domain.subdomains) {
243
+ if (typeof sub.name !== 'string' || sub.name.length === 0) {
244
+ // Recursão vai validar; ainda assim, evitar set com chave estranha.
245
+ validateDomain(sub, fullPath)
246
+ continue
247
+ }
248
+ if (seen.has(sub.name)) {
249
+ throw error({
250
+ code: 'domain.duplicate_subdomain',
251
+ category: 'internal',
252
+ message: `Subdomain "${sub.name}" duplicado em "${fullPathStr}"`,
253
+ })
254
+ }
255
+ seen.add(sub.name)
256
+ validateDomain(sub, fullPath)
257
+ }
258
+ }
259
+
260
+ // — actions: nomes únicos no domínio (e subdomínios) —
261
+ // (Roda *depois* dos subdomains: assim erros de nome em subs aparecem
262
+ // primeiro, antes de detectar duplicatas cruzadas.)
263
+ const collected: FlattenedDomain = { actions: [], reactions: [], schedules: [] }
264
+ collect(domain, collected)
265
+
266
+ const actionNames = new Set<string>()
267
+ for (const action of collected.actions) {
268
+ if (actionNames.has(action.name)) {
269
+ throw error({
270
+ code: 'domain.duplicate_action',
271
+ category: 'internal',
272
+ message: `Action "${action.name}" duplicada em "${fullPathStr}"`,
273
+ })
274
+ }
275
+ actionNames.add(action.name)
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Caminha a árvore emitindo warnings de nomenclatura. `path` é a trilha de
281
+ * ancestrais (excluindo o próprio `domain`).
282
+ */
283
+ function walkForWarnings(domain: DomainConfig, path: string[]): void {
284
+ const fullPath = [...path, domain.name]
285
+ const fullPathStr = fullPath.join('.')
286
+ for (const action of iterateActions(domain.actions)) {
287
+ if (!actionNameMatchesPath(action.name, fullPath)) {
288
+ console.warn(
289
+ `[opus] Action "${action.name}" não casa com prefixo "${fullPathStr}." — considere renomear.`,
290
+ )
291
+ }
292
+ }
293
+ if (domain.subdomains !== undefined) {
294
+ for (const sub of domain.subdomains) walkForWarnings(sub, fullPath)
295
+ }
296
+ }
297
+
298
+ /**
299
+ * `true` se `actionName` começa com `<domain>.` ou `<domain>.<sub>.` etc,
300
+ * e ainda tem um segmento de verbo no final.
301
+ */
302
+ function actionNameMatchesPath(actionName: string, path: string[]): boolean {
303
+ const parts = actionName.split('.')
304
+ // Precisa de pelo menos `domain.verb`.
305
+ if (parts.length < path.length + 1) return false
306
+ for (let i = 0; i < path.length; i++) {
307
+ if (parts[i] !== path[i]) return false
308
+ }
309
+ return true
310
+ }