@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,765 @@
1
+ /**
2
+ * @softize/opus/schema/zod — Zod-based logical type factories.
3
+ *
4
+ * Catálogo de factories que retornam `LogicalType<T>`: um objeto que carrega
5
+ * o schema Zod subjacente (`zod()`), metadata logical (`meta`), e os métodos
6
+ * `parse / format / validate` pra operar sobre instâncias do tipo canônico.
7
+ *
8
+ * Adapters (DB, UI, OpenAPI) continuam lendo a metadata via `getLogicalType()`
9
+ * sobre o schema Zod (recuperado por `.zod()`) pra mapear semântica em
10
+ * comportamento concreto.
11
+ *
12
+ * Uso:
13
+ * import { z } from 'zod'
14
+ * import { t } from '@softize/opus/schema/zod'
15
+ *
16
+ * const Email = t.email()
17
+ * Email.parse(' Foo@Bar.COM ') // → 'foo@bar.com'
18
+ * Email.validate('not-an-email') // → { ok: false, error: '...' }
19
+ *
20
+ * defineAction({
21
+ * input: z.object({
22
+ * email: t.email().zod(),
23
+ * amount: t.money({ currency: 'BRL' }).zod(),
24
+ * paidAt: t.datetime().zod(),
25
+ * }),
26
+ * })
27
+ */
28
+
29
+ import { z, type ZodTypeAny, type ZodSchema } from 'zod'
30
+ import { attachLogicalType, type LogicalTypeMeta } from '../index.ts'
31
+ import {
32
+ formatDatetime,
33
+ formatDate,
34
+ formatDecimal,
35
+ formatMoney,
36
+ parseDecimal,
37
+ parseMoney,
38
+ } from '../format/locale.ts'
39
+
40
+ // =============================================================================
41
+ // Interface pública: LogicalType<T>
42
+ // =============================================================================
43
+
44
+ export type ValidateResult<T> =
45
+ | { ok: true; value: T }
46
+ | { ok: false; error: string }
47
+
48
+ export interface FormatOptions {
49
+ locale?: string
50
+ }
51
+
52
+ export interface LogicalType<T> {
53
+ /** Schema Zod subjacente — pra reuso em z.object({ field: t.email().zod() }). */
54
+ zod(): ZodSchema<T>
55
+ /** Metadata: { logicalType, params? }. */
56
+ meta: LogicalTypeMeta
57
+ /** Parse de input cru (form value, query param, JSON) pro tipo canônico T. */
58
+ parse(input: unknown): T
59
+ /**
60
+ * Format pra display humano. Locale default 'pt-BR' onde aplicável
61
+ * (datetime, money, percent, decimal). Tipos string/url/uuid/etc
62
+ * retornam `String(value)`.
63
+ */
64
+ format(value: T, options?: FormatOptions): string
65
+ /** Validação completa. Retorna ok:true com valor parseado, ou ok:false com erro humano. */
66
+ validate(value: unknown): ValidateResult<T>
67
+
68
+ // — Modifiers de coluna (entidade) — encadeáveis, retornam novo LogicalType —
69
+ /** Metadata de coluna acumulada pelos modifiers. Lida pelo data/migration adapter. */
70
+ readonly column: ColumnMeta
71
+ /** Marca como primary key. Convenção: encadeie `.pk()` por último. */
72
+ pk(): LogicalType<T> & { readonly __pk: true }
73
+ /** Constraint UNIQUE. */
74
+ unique(): LogicalType<T>
75
+ /** Coluna NULL-able. Muda o tipo inferido pra `T | null`. */
76
+ nullable(): LogicalType<T | null>
77
+ /** Índice nesta coluna. `using` é hint de tipo (gin, etc) que o adapter resolve. */
78
+ index(opts?: { using?: string }): LogicalType<T>
79
+ /** Default no DDL (e no schema). Torna o campo opcional no insert. */
80
+ default(value: NonNullable<T>): LogicalType<T> & { readonly __hasDefault: true }
81
+ /** Foreign key pra outra entidade (por nome). `field` default = pk do alvo. */
82
+ references(entity: string, field?: string): LogicalType<T>
83
+ /** Doc de negócio do campo (a fonte de entendimento). Encadeável; flui pro manifest. */
84
+ doc(text: string): LogicalType<T>
85
+ }
86
+
87
+ /**
88
+ * Metadata de coluna acumulada pelos modifiers de entidade. Canal separado da
89
+ * validação (Zod): o data/migration adapter lê isto pra emitir o DDL.
90
+ */
91
+ export interface ColumnMeta {
92
+ pk?: boolean
93
+ unique?: boolean
94
+ nullable?: boolean
95
+ index?: boolean | { using?: string }
96
+ hasDefault?: boolean
97
+ default?: unknown
98
+ references?: { entity: string; field?: string }
99
+ /** Doc de negócio do campo — a fonte de entendimento. Flui pro manifest/lente. */
100
+ doc?: string
101
+ }
102
+
103
+ // =============================================================================
104
+ // Helper interno: build LogicalType
105
+ // =============================================================================
106
+
107
+ interface BuildArgs<T> {
108
+ logicalType: string
109
+ params?: Record<string, unknown>
110
+ zod: ZodSchema<T>
111
+ parse: (input: unknown) => T
112
+ format?: (value: T, options?: FormatOptions) => string
113
+ }
114
+
115
+ interface FieldCore<T> {
116
+ zod: ZodSchema<T>
117
+ meta: LogicalTypeMeta
118
+ parse: (input: unknown) => T
119
+ format: (value: T, options?: FormatOptions) => string
120
+ }
121
+
122
+ function build<T>(args: BuildArgs<T>): LogicalType<T> {
123
+ const meta: LogicalTypeMeta =
124
+ args.params === undefined
125
+ ? { logicalType: args.logicalType }
126
+ : { logicalType: args.logicalType, params: args.params }
127
+ // Mantém o contrato antigo: metadata anexada ao schema Zod também,
128
+ // pra adapters (openapi, db, ui) que leem via getLogicalType().
129
+ attachLogicalType(args.zod as unknown as object, meta)
130
+
131
+ const format = args.format ?? ((value: T): string => String(value))
132
+ return makeField({ zod: args.zod, meta, parse: args.parse, format }, {})
133
+ }
134
+
135
+ /**
136
+ * Constrói o `LogicalType` imutável a partir do core + metadata de coluna.
137
+ * Cada modifier (`pk`, `nullable`, ...) devolve um novo `LogicalType`, sem
138
+ * mutar o anterior — encadeamento é puro.
139
+ */
140
+ function makeField<T>(core: FieldCore<T>, column: ColumnMeta): LogicalType<T> {
141
+ return {
142
+ zod: () => core.zod,
143
+ meta: core.meta,
144
+ parse: core.parse,
145
+ format: core.format,
146
+ column,
147
+ validate(value: unknown): ValidateResult<T> {
148
+ try {
149
+ const parsed = core.parse(value)
150
+ const result = core.zod.safeParse(parsed)
151
+ if (result.success) return { ok: true, value: result.data }
152
+ const issue = result.error.issues[0]
153
+ return { ok: false, error: humanizeIssue(issue) }
154
+ } catch (err) {
155
+ return { ok: false, error: humanizeError(err) }
156
+ }
157
+ },
158
+ pk: () =>
159
+ makeField(core, { ...column, pk: true }) as LogicalType<T> & {
160
+ readonly __pk: true
161
+ },
162
+ unique: () => makeField(core, { ...column, unique: true }),
163
+ nullable: () =>
164
+ makeField<T | null>(
165
+ {
166
+ zod: core.zod.nullable() as unknown as ZodSchema<T | null>,
167
+ meta: core.meta,
168
+ parse: (input) => (input === null || input === undefined ? null : core.parse(input)),
169
+ format: (value, options) => (value === null ? '' : core.format(value, options)),
170
+ },
171
+ { ...column, nullable: true },
172
+ ),
173
+ index: (opts) =>
174
+ makeField(core, {
175
+ ...column,
176
+ index: opts?.using !== undefined ? { using: opts.using } : true,
177
+ }),
178
+ default: (value) =>
179
+ makeField(core, { ...column, hasDefault: true, default: value }) as LogicalType<T> & {
180
+ readonly __hasDefault: true
181
+ },
182
+ references: (entity, field) =>
183
+ makeField(core, {
184
+ ...column,
185
+ references: field !== undefined ? { entity, field } : { entity },
186
+ }),
187
+ doc: (text) => makeField(core, { ...column, doc: text }),
188
+ }
189
+ }
190
+
191
+ function humanizeIssue(issue: z.ZodIssue | undefined): string {
192
+ /* v8 ignore next */
193
+ if (issue === undefined) return 'valor inválido'
194
+ return issue.message
195
+ }
196
+
197
+ function humanizeError(err: unknown): string {
198
+ /* v8 ignore next 2 */
199
+ if (!(err instanceof Error)) return 'valor inválido'
200
+ return err.message
201
+ }
202
+
203
+ // =============================================================================
204
+ // Catálogo: primitivos
205
+ // =============================================================================
206
+
207
+ const string = (): LogicalType<string> =>
208
+ build({
209
+ logicalType: 'string',
210
+ zod: z.string(),
211
+ parse: (input) => {
212
+ if (typeof input === 'string') return input
213
+ throw new Error('esperava string')
214
+ },
215
+ })
216
+
217
+ const text = (): LogicalType<string> =>
218
+ build({
219
+ logicalType: 'text',
220
+ zod: z.string(),
221
+ parse: (input) => {
222
+ if (typeof input === 'string') return input
223
+ throw new Error('esperava texto')
224
+ },
225
+ })
226
+
227
+ const int = (): LogicalType<number> =>
228
+ build({
229
+ logicalType: 'int',
230
+ zod: z.number().int(),
231
+ parse: (input) => {
232
+ if (typeof input === 'number') return input
233
+ if (typeof input === 'string' && input.trim() !== '') {
234
+ const n = Number(input)
235
+ if (!Number.isNaN(n)) return n
236
+ }
237
+ throw new Error('esperava inteiro')
238
+ },
239
+ })
240
+
241
+ const bigint_ = (): LogicalType<bigint> =>
242
+ build({
243
+ logicalType: 'bigint',
244
+ zod: z.bigint(),
245
+ parse: (input) => {
246
+ if (typeof input === 'bigint') return input
247
+ if (typeof input === 'number' && Number.isInteger(input)) return BigInt(input)
248
+ if (typeof input === 'string' && /^-?\d+$/.test(input.trim())) {
249
+ return BigInt(input.trim())
250
+ }
251
+ throw new Error('esperava bigint')
252
+ },
253
+ })
254
+
255
+ interface DecimalParams {
256
+ precision?: number
257
+ scale?: number
258
+ }
259
+
260
+ const decimal = (params?: DecimalParams): LogicalType<string> => {
261
+ const zodSchema = z.string().regex(/^-?\d+(\.\d+)?$/, 'esperava decimal')
262
+ return build({
263
+ logicalType: 'decimal',
264
+ ...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
265
+ zod: zodSchema,
266
+ parse: (input) => {
267
+ if (typeof input === 'number') return String(input)
268
+ if (typeof input === 'string') return parseDecimal(input)
269
+ throw new Error('esperava decimal')
270
+ },
271
+ format: (value, options) => formatDecimal(value, options?.locale),
272
+ })
273
+ }
274
+
275
+ const boolean = (): LogicalType<boolean> =>
276
+ build({
277
+ logicalType: 'boolean',
278
+ zod: z.boolean(),
279
+ parse: (input) => {
280
+ if (typeof input === 'boolean') return input
281
+ if (input === 1 || input === 0) return input === 1
282
+ if (typeof input === 'string') {
283
+ const v = input.trim().toLowerCase()
284
+ if (v === 'true' || v === '1') return true
285
+ if (v === 'false' || v === '0') return false
286
+ }
287
+ throw new Error('esperava boolean')
288
+ },
289
+ })
290
+
291
+ const json = (): LogicalType<unknown> =>
292
+ build({
293
+ logicalType: 'json',
294
+ zod: z.unknown(),
295
+ parse: (input) => input,
296
+ })
297
+
298
+ // =============================================================================
299
+ // Catálogo: tempo
300
+ // =============================================================================
301
+
302
+ interface DatetimeParams {
303
+ precision?: 'second' | 'millisecond'
304
+ offset?: boolean
305
+ }
306
+
307
+ const datetime = (params?: DatetimeParams): LogicalType<string> => {
308
+ const options: { offset: boolean; precision?: number } = {
309
+ offset: params?.offset ?? true,
310
+ }
311
+ if (params?.precision === 'millisecond') options.precision = 3
312
+ const zodSchema = z.string().datetime(options)
313
+ return build({
314
+ logicalType: 'datetime',
315
+ ...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
316
+ zod: zodSchema,
317
+ parse: (input) => {
318
+ if (input instanceof Date) return input.toISOString()
319
+ if (typeof input === 'string') return input
320
+ throw new Error('esperava datetime ISO ou Date')
321
+ },
322
+ format: (value, opts) => {
323
+ if (opts?.locale === undefined) return value
324
+ return formatDatetime(value, opts.locale)
325
+ },
326
+ })
327
+ }
328
+
329
+ const date = (): LogicalType<string> =>
330
+ build({
331
+ logicalType: 'date',
332
+ zod: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'esperava YYYY-MM-DD'),
333
+ parse: (input) => {
334
+ if (input instanceof Date) {
335
+ // Componentes UTC pra evitar drift de timezone.
336
+ const y = input.getUTCFullYear()
337
+ const m = String(input.getUTCMonth() + 1).padStart(2, '0')
338
+ const d = String(input.getUTCDate()).padStart(2, '0')
339
+ return `${y}-${m}-${d}`
340
+ }
341
+ if (typeof input === 'string') return input
342
+ throw new Error('esperava data')
343
+ },
344
+ format: (value, opts) => {
345
+ if (opts?.locale === undefined) return value
346
+ return formatDate(value, opts.locale)
347
+ },
348
+ })
349
+
350
+ const time = (): LogicalType<string> =>
351
+ build({
352
+ logicalType: 'time',
353
+ zod: z
354
+ .string()
355
+ .regex(/^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/, 'esperava HH:mm[:ss]'),
356
+ parse: (input) => {
357
+ if (typeof input === 'string') return input
358
+ throw new Error('esperava time')
359
+ },
360
+ })
361
+
362
+ const duration = (): LogicalType<string> =>
363
+ build({
364
+ logicalType: 'duration',
365
+ zod: z.string().duration(),
366
+ parse: (input) => {
367
+ if (typeof input === 'string') return input
368
+ throw new Error('esperava duration ISO 8601')
369
+ },
370
+ })
371
+
372
+ const timezone = (): LogicalType<string> =>
373
+ build({
374
+ logicalType: 'timezone',
375
+ zod: z
376
+ .string()
377
+ .regex(/^[A-Z][A-Za-z_]+\/[A-Z][A-Za-z_/]+$/, 'esperava IANA tz'),
378
+ parse: (input) => {
379
+ if (typeof input === 'string') return input
380
+ throw new Error('esperava timezone')
381
+ },
382
+ })
383
+
384
+ // =============================================================================
385
+ // Catálogo: identificadores & formatos
386
+ // =============================================================================
387
+
388
+ const email = (): LogicalType<string> => {
389
+ const zodSchema = z
390
+ .string()
391
+ .trim()
392
+ .toLowerCase()
393
+ .pipe(z.string().email('email inválido')) as unknown as ZodSchema<string>
394
+ return build({
395
+ logicalType: 'email',
396
+ zod: zodSchema,
397
+ parse: (input) => {
398
+ if (typeof input === 'string') return input.trim().toLowerCase()
399
+ throw new Error('esperava email')
400
+ },
401
+ })
402
+ }
403
+
404
+ interface PhoneParams {
405
+ country?: string
406
+ }
407
+
408
+ const phone = (params?: PhoneParams): LogicalType<string> => {
409
+ const zodSchema = z
410
+ .string()
411
+ .regex(/^\+\d{8,15}$/, 'esperava telefone E.164 (+ e 8-15 dígitos)')
412
+ return build({
413
+ logicalType: 'phone',
414
+ ...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
415
+ zod: zodSchema,
416
+ parse: (input) => {
417
+ if (typeof input === 'string') return input.trim()
418
+ throw new Error('esperava telefone')
419
+ },
420
+ })
421
+ }
422
+
423
+ interface UrlParams {
424
+ protocols?: string[]
425
+ }
426
+
427
+ const url = (params?: UrlParams): LogicalType<string> => {
428
+ const allowed = new Set(params?.protocols ?? ['http', 'https'])
429
+ const zodSchema = z
430
+ .string()
431
+ .url('URL inválida')
432
+ .refine(
433
+ (value) => {
434
+ try {
435
+ return allowed.has(new URL(value).protocol.replace(':', ''))
436
+ } catch {
437
+ /* v8 ignore next */
438
+ return false
439
+ }
440
+ },
441
+ { message: `protocolo deve ser um de: ${[...allowed].join(', ')}` },
442
+ )
443
+ return build({
444
+ logicalType: 'url',
445
+ ...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
446
+ zod: zodSchema as unknown as ZodSchema<string>,
447
+ parse: (input) => {
448
+ if (input instanceof URL) return input.toString()
449
+ if (typeof input === 'string') return input.trim()
450
+ throw new Error('esperava URL')
451
+ },
452
+ })
453
+ }
454
+
455
+ const uuid = (): LogicalType<string> =>
456
+ build({
457
+ logicalType: 'uuid',
458
+ zod: z.string().uuid('UUID inválido'),
459
+ parse: (input) => {
460
+ if (typeof input === 'string') return input.trim().toLowerCase()
461
+ throw new Error('esperava UUID')
462
+ },
463
+ })
464
+
465
+ const slug = (): LogicalType<string> =>
466
+ build({
467
+ logicalType: 'slug',
468
+ zod: z
469
+ .string()
470
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'esperava slug kebab-case'),
471
+ parse: (input) => {
472
+ if (typeof input === 'string') return input.trim()
473
+ throw new Error('esperava slug')
474
+ },
475
+ })
476
+
477
+ // =============================================================================
478
+ // Catálogo: domínio
479
+ // =============================================================================
480
+
481
+ interface MoneyParams {
482
+ currency?: string
483
+ }
484
+
485
+ const money = (params?: MoneyParams): LogicalType<bigint> => {
486
+ const currencyCode = params?.currency ?? 'BRL'
487
+ return build({
488
+ logicalType: 'money',
489
+ ...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
490
+ zod: z.bigint(),
491
+ parse: (input) => {
492
+ if (typeof input === 'bigint') return input
493
+ if (typeof input === 'number' && Number.isInteger(input)) return BigInt(input)
494
+ if (typeof input === 'string') {
495
+ const cents = parseMoney(input)
496
+ if (cents !== null) return cents
497
+ }
498
+ throw new Error('esperava valor monetário')
499
+ },
500
+ format: (value, opts) => formatMoney(value, currencyCode, opts?.locale),
501
+ })
502
+ }
503
+
504
+ const currency = (): LogicalType<string> =>
505
+ build({
506
+ logicalType: 'currency',
507
+ zod: z.string().regex(/^[A-Z]{3}$/, 'esperava ISO 4217'),
508
+ parse: (input) => {
509
+ if (typeof input === 'string') return input.trim().toUpperCase()
510
+ throw new Error('esperava código de moeda')
511
+ },
512
+ })
513
+
514
+ const country = (): LogicalType<string> =>
515
+ build({
516
+ logicalType: 'country',
517
+ zod: z.string().regex(/^[A-Z]{2}$/, 'esperava ISO 3166 alpha-2'),
518
+ parse: (input) => {
519
+ if (typeof input === 'string') return input.trim().toUpperCase()
520
+ throw new Error('esperava país')
521
+ },
522
+ })
523
+
524
+ const locale_ = (): LogicalType<string> =>
525
+ build({
526
+ logicalType: 'locale',
527
+ zod: z.string().regex(/^[a-z]{2,3}(-[A-Z]{2})?$/, 'esperava locale BCP 47'),
528
+ parse: (input) => {
529
+ if (typeof input === 'string') return input.trim()
530
+ throw new Error('esperava locale')
531
+ },
532
+ })
533
+
534
+ const percent = (): LogicalType<number> =>
535
+ build({
536
+ logicalType: 'percent',
537
+ // Convenção: 0..100 (display friendly). Schemas que queiram 0..1
538
+ // podem usar t.decimal() ou similar.
539
+ zod: z.number(),
540
+ parse: (input) => {
541
+ if (typeof input === 'number') return input
542
+ if (typeof input === 'string') {
543
+ const cleaned = input.trim().replace('%', '').replace(',', '.')
544
+ const n = Number(cleaned)
545
+ if (!Number.isNaN(n)) return n
546
+ }
547
+ throw new Error('esperava percent')
548
+ },
549
+ format: (value, opts) => {
550
+ const locale = opts?.locale ?? 'pt-BR'
551
+ return new Intl.NumberFormat(locale, {
552
+ style: 'percent',
553
+ minimumFractionDigits: 0,
554
+ maximumFractionDigits: 2,
555
+ }).format(value / 100)
556
+ },
557
+ })
558
+
559
+ // =============================================================================
560
+ // Catálogo: texto rico
561
+ // =============================================================================
562
+
563
+ const markdown = (): LogicalType<string> =>
564
+ build({
565
+ logicalType: 'markdown',
566
+ zod: z.string(),
567
+ parse: (input) => {
568
+ if (typeof input === 'string') return input
569
+ throw new Error('esperava markdown')
570
+ },
571
+ })
572
+
573
+ const html = (): LogicalType<string> =>
574
+ build({
575
+ logicalType: 'html',
576
+ zod: z.string(),
577
+ parse: (input) => {
578
+ if (typeof input === 'string') return input
579
+ throw new Error('esperava HTML')
580
+ },
581
+ })
582
+
583
+ // =============================================================================
584
+ // Catálogo: composição
585
+ // =============================================================================
586
+
587
+ const enum_ = <T extends [string, ...string[]]>(values: T): LogicalType<T[number]> => {
588
+ const zodSchema = z.enum(values)
589
+ return build<T[number]>({
590
+ logicalType: 'enum',
591
+ params: { values },
592
+ zod: zodSchema as unknown as ZodSchema<T[number]>,
593
+ parse: (input) => {
594
+ if (typeof input === 'string' && (values as readonly string[]).includes(input)) {
595
+ return input as T[number]
596
+ }
597
+ throw new Error(`esperava um de: ${values.join(', ')}`)
598
+ },
599
+ })
600
+ }
601
+
602
+ /**
603
+ * Aceita ZodSchema ou LogicalType<U>. Internamente extrai o Zod schema
604
+ * via `.zod()` quando recebe LogicalType.
605
+ */
606
+ const array = <U>(of: LogicalType<U> | ZodTypeAny): LogicalType<U[]> => {
607
+ const inner = isLogicalType(of) ? of.zod() : (of as unknown as ZodSchema<U>)
608
+ const zodSchema = z.array(inner)
609
+ return build({
610
+ logicalType: 'array',
611
+ zod: zodSchema,
612
+ parse: (input) => {
613
+ if (Array.isArray(input)) return input as U[]
614
+ throw new Error('esperava array')
615
+ },
616
+ })
617
+ }
618
+
619
+ const object = <S extends z.ZodRawShape>(
620
+ shape: S,
621
+ ): LogicalType<z.infer<z.ZodObject<S>>> => {
622
+ const zodSchema = z.object(shape)
623
+ return build({
624
+ logicalType: 'object',
625
+ zod: zodSchema as unknown as ZodSchema<z.infer<z.ZodObject<S>>>,
626
+ parse: (input) => {
627
+ if (input !== null && typeof input === 'object') {
628
+ return input as z.infer<z.ZodObject<S>>
629
+ }
630
+ throw new Error('esperava objeto')
631
+ },
632
+ })
633
+ }
634
+
635
+ function isLogicalType(value: unknown): value is LogicalType<unknown> {
636
+ return (
637
+ value !== null &&
638
+ typeof value === 'object' &&
639
+ 'zod' in value &&
640
+ typeof (value as { zod: unknown }).zod === 'function'
641
+ )
642
+ }
643
+
644
+ // =============================================================================
645
+ // t.dict — primeira classe
646
+ // =============================================================================
647
+
648
+ /**
649
+ * Meta de cada chave de um dict. `label` obrigatório; resto livre. `doc` é o
650
+ * entendimento de negócio da chave (o que aquele estado significa, quando se
651
+ * entra/sai) — a fonte rica de consulta rápida; flui pro manifest/lente.
652
+ */
653
+ export interface DictEntryMeta {
654
+ label: string
655
+ doc?: string
656
+ color?: string
657
+ icon?: string
658
+ order?: number
659
+ [key: string]: unknown
660
+ }
661
+
662
+ /**
663
+ * Option pronta pra UI: combina key + meta da entrada.
664
+ */
665
+ export type DictOption<K extends string, M extends DictEntryMeta> = M & {
666
+ value: K
667
+ }
668
+
669
+ export interface DictType<K extends string, M extends DictEntryMeta> {
670
+ /** Schema Zod equivalente: `z.enum(keys)`. */
671
+ zod(): z.ZodEnum<[K, ...K[]]>
672
+ /** Metadata logical: { logicalType: 'dict', params: { keys, entries } }. */
673
+ meta: LogicalTypeMeta
674
+ /** Lista de chaves na ordem de declaração. */
675
+ keys(): K[]
676
+ /**
677
+ * Label da chave. Segundo arg é placeholder pra i18n — no v1 sempre retorna
678
+ * `entries[key].label` (locale ignorado). Documentado como dívida técnica.
679
+ */
680
+ labelFor(key: K, locale?: string): string
681
+ /** Meta completa da chave (label + extras). */
682
+ metaFor(key: K): M
683
+ /** Lista pronta pra `<Select options={...} />`: `{ value, ...meta }`. */
684
+ options(): DictOption<K, M>[]
685
+ /** True se `key` está no dict. */
686
+ has(key: string): key is K
687
+ /** Doc de negócio do dicionário inteiro (o que esse vocabulário representa). */
688
+ doc?: string
689
+ }
690
+
691
+ /** Opções do dict: `doc` = o entendimento do vocabulário como um todo. */
692
+ export interface DictOpts {
693
+ doc?: string
694
+ }
695
+
696
+ const dict = <const M extends Record<string, DictEntryMeta>>(
697
+ entries: M,
698
+ opts?: DictOpts,
699
+ ): DictType<Extract<keyof M, string>, M[keyof M]> => {
700
+ type K = Extract<keyof M, string>
701
+ const keys = Object.keys(entries) as K[]
702
+ if (keys.length === 0) {
703
+ throw new Error('t.dict precisa de pelo menos uma entrada')
704
+ }
705
+ const zodSchema = z.enum(keys as [K, ...K[]])
706
+ const meta: LogicalTypeMeta = {
707
+ logicalType: 'dict',
708
+ params: { keys, entries, ...(opts?.doc !== undefined ? { doc: opts.doc } : {}) },
709
+ }
710
+ attachLogicalType(zodSchema as unknown as object, meta)
711
+
712
+ return {
713
+ zod: () => zodSchema,
714
+ meta,
715
+ ...(opts?.doc !== undefined ? { doc: opts.doc } : {}),
716
+ keys: () => [...keys],
717
+ labelFor: (key, _locale) => {
718
+ // _locale reservado pra i18n futuro; no v1 sempre retorna o label cru.
719
+ return (entries[key] as M[keyof M]).label
720
+ },
721
+ metaFor: (key) => entries[key] as M[keyof M],
722
+ options: () =>
723
+ keys.map((k) => ({
724
+ ...(entries[k] as M[keyof M]),
725
+ value: k,
726
+ })) as DictOption<K, M[keyof M]>[],
727
+ has: (key: string): key is K =>
728
+ Object.prototype.hasOwnProperty.call(entries, key),
729
+ }
730
+ }
731
+
732
+ // =============================================================================
733
+ // Namespace exportado
734
+ // =============================================================================
735
+
736
+ export const t = {
737
+ string,
738
+ text,
739
+ int,
740
+ bigint: bigint_,
741
+ decimal,
742
+ boolean,
743
+ json,
744
+ datetime,
745
+ date,
746
+ time,
747
+ duration,
748
+ timezone,
749
+ email,
750
+ phone,
751
+ url,
752
+ uuid,
753
+ slug,
754
+ money,
755
+ currency,
756
+ country,
757
+ locale: locale_,
758
+ percent,
759
+ markdown,
760
+ html,
761
+ enum: enum_,
762
+ array,
763
+ object,
764
+ dict,
765
+ } as const