@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,85 @@
1
+ /**
2
+ * opus mcp — server MCP que expõe a base ao agente: introspecção (fonte única) +
3
+ * a régua (opus check) + o gerador (create-action). Os agentes apontam pra cá via
4
+ * `mcp_config`. Embrulha os engines já testados (introspect/check/scaffold).
5
+ *
6
+ * Tools:
7
+ * opus_introspect — modelo da estrutura + wiring (o que existe, ao vivo)
8
+ * opus_check — valida convenções (findings)
9
+ * opus_create_action — gera o esqueleto canônico de uma action
10
+ * opus_list_components — catálogo da UI (o que existe, quando usar, o que importar)
11
+ * opus_get_component — detalhe de um componente pelo nome
12
+ */
13
+
14
+ import path from 'node:path'
15
+ import { z } from 'zod'
16
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
17
+ import { introspect } from './introspect.mjs'
18
+ import { scanDir } from './check.mjs'
19
+ import { actionTemplate } from '../../registry/skills/create-action/scaffold.mjs'
20
+ import { listComponents, getComponent } from './components.mjs'
21
+
22
+ const asText = (data) => ({
23
+ content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }],
24
+ })
25
+ const dirOf = (d) => path.resolve(process.cwd(), d ?? '.')
26
+
27
+ /** Monta o McpServer com as tools da base. Exportado pra testar (transport in-memory). */
28
+ export function buildServer() {
29
+ const server = new McpServer({ name: 'opus', version: '0.0.0' })
30
+
31
+ server.registerTool(
32
+ 'opus_introspect',
33
+ {
34
+ description: 'Modelo da estrutura do projeto Opus: actions/reactions/schedules + wiring (causalidade).',
35
+ inputSchema: { dir: z.string().optional() },
36
+ },
37
+ async ({ dir }) => asText(await introspect(dirOf(dir))),
38
+ )
39
+
40
+ server.registerTool(
41
+ 'opus_check',
42
+ {
43
+ description: 'Valida as convenções das actions (régua) — defineAction e o split defineContract+bindAction: naming, kind, ordem dos campos, export, requires-sem-authorize. Retorna findings.',
44
+ inputSchema: { dir: z.string().optional() },
45
+ },
46
+ async ({ dir }) => asText(await scanDir(dirOf(dir))),
47
+ )
48
+
49
+ server.registerTool(
50
+ 'opus_create_action',
51
+ {
52
+ description: 'Gera o esqueleto canônico de uma opus action (passa no opus check por construção).',
53
+ inputSchema: {
54
+ resource: z.string(),
55
+ verb: z.string(),
56
+ kind: z.enum(['simple', 'form', 'list', 'view']).optional(),
57
+ },
58
+ },
59
+ async ({ resource, verb, kind }) => asText(actionTemplate({ resource, verb, kind })),
60
+ )
61
+
62
+ server.registerTool(
63
+ 'opus_list_components',
64
+ {
65
+ description:
66
+ 'Catálogo da UI do Opus: cada componente com altitude, quando-usar, o que importar e as variantes (cva: variant/size + defaults). Use antes de criar UI na mão (evita reinventar primitivo que já existe ou chutar variante).',
67
+ inputSchema: {},
68
+ },
69
+ async () => asText(await listComponents()),
70
+ )
71
+
72
+ server.registerTool(
73
+ 'opus_get_component',
74
+ {
75
+ description: 'Detalhe de um componente da UI pelo nome canônico (ex.: button, dropdown-menu).',
76
+ inputSchema: { name: z.string() },
77
+ },
78
+ async ({ name }) => {
79
+ const c = await getComponent(name)
80
+ return asText(c ?? `Componente "${name}" não existe. Use opus_list_components pra ver o catálogo.`)
81
+ },
82
+ )
83
+
84
+ return server
85
+ }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall do @softize/opus — auto-bootstrap: roda `opus setup` no projeto
4
+ * consumidor assim que a base é instalada. Assim a DEPENDÊNCIA é o sinal: quem
5
+ * adiciona @softize/opus já nasce conhecendo a base, sem precisar saber do init.
6
+ *
7
+ * Blindagem (best-effort, NUNCA falha o install):
8
+ * • OPUS_SKIP_INIT=1 → pula
9
+ * • projeto = INIT_CWD (onde o install rodou); fallback cwd
10
+ * • pula o próprio repo do Opus (dev do pacote)
11
+ * • só roda se o consumidor tem @softize/opus nas deps (evita repo aleatório)
12
+ *
13
+ * Nota pnpm: por segurança o pnpm NÃO roda build scripts de dependência por
14
+ * padrão — o consumidor precisa allowlistar (`pnpm.onlyBuiltDependencies:
15
+ * ["@softize/opus"]`) ou aprovar. Sob npm roda direto.
16
+ */
17
+
18
+ import { promises as fs } from 'node:fs'
19
+ import path from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ import { initProject } from './init.mjs'
23
+
24
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
25
+ const REGISTRY_DIR = path.join(PACKAGE_ROOT, 'registry')
26
+
27
+ async function main() {
28
+ if (process.env.OPUS_SKIP_INIT) return
29
+
30
+ const projectDir = process.env.INIT_CWD || process.cwd()
31
+ // Não inicializa o próprio Opus.
32
+ if (projectDir === PACKAGE_ROOT || projectDir.startsWith(PACKAGE_ROOT + path.sep)) return
33
+
34
+ let pkg
35
+ try {
36
+ pkg = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf-8'))
37
+ } catch {
38
+ return // Sem package.json → não é projeto.
39
+ }
40
+ if (pkg.name === '@softize/opus') return
41
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
42
+ if (!deps['@softize/opus']) return // Não é consumidor da base.
43
+
44
+ const r = await initProject(REGISTRY_DIR, projectDir)
45
+ const n = r.created.length + r.synced.length
46
+ console.log(
47
+ `\x1b[36m[opus]\x1b[0m base v${r.version} — ${r.wasInitialized ? 'sincronizada' : 'inicializada'}` +
48
+ (n ? ` (${n} arquivo(s))` : '') +
49
+ `. Veja CLAUDE.md / opus.json.`,
50
+ )
51
+ }
52
+
53
+ main().catch((e) => {
54
+ // O postinstall jamais derruba o install.
55
+ console.log(`\x1b[33m[opus]\x1b[0m init pulado: ${e?.message ?? e}`)
56
+ })
@@ -0,0 +1,85 @@
1
+ # Proposta: protocolo de eventos de conversa (streaming no `<Chat>` e na camada ai)
2
+
3
+ > **Status: APROVADO pelo João e IMPLEMENTADO (2.22.0).** Desenho da issue
4
+ > [softize-dev/opus#5](https://github.com/softize-dev/opus/issues/5). Implementação:
5
+ > `ChatEvent` no core, `runStream` no driver anthropic e no `BoundAi`, `<Chat>` consumindo
6
+ > os dois modos (docs vivas ai/chat atualizadas; 3 testes novos).
7
+
8
+ ## O problema
9
+
10
+ A casa tem três chats artesanais com o mesmo contrato implícito e três implementações:
11
+
12
+ | Consumidor | Transporte | O que precisa expressar |
13
+ |---|---|---|
14
+ | ChatRail do Maestro | SSE (replay idempotente, heartbeat) | texto incremental · tool em uso · fim de turno |
15
+ | Vetra BI (bi-poc) | SSE (fetch stream) | texto incremental · tool em uso · **artefato** (card de relatório) · fim de turno |
16
+ | Copilot / `<Chat>` atual | request/response | resposta inteira |
17
+
18
+ O `<Chat>` (2.17) só fala o terceiro caso (`send(messages) => Promise<string>`), então os
19
+ outros dois reimplementam bolha, indicador e transporte por fora — o mesmo filme do
20
+ AppShell antes do 2.21.0. A doc do ai layer já previa: *"streaming entra quando um caso
21
+ real cobrar"*. Cobrou duas vezes.
22
+
23
+ ## O contrato proposto
24
+
25
+ O evento de conversa — o mínimo que os dois casos reais exigiram, nada além:
26
+
27
+ ```ts
28
+ export type ChatEvent =
29
+ /** Texto incremental do agente (acumula na mensagem corrente). */
30
+ | { type: 'text'; delta: string }
31
+ /** Tool em uso — dado cru; a apresentação (humanizar, agrupar) é do componente. */
32
+ | { type: 'tool'; name: string; detail?: string }
33
+ /** Artefato produzido na conversa (relatório, arquivo, url) — render é do app. */
34
+ | { type: 'artifact'; kind: string; ref: string; title?: string }
35
+ /** Fim do turno. */
36
+ | { type: 'done'; ok: boolean; error?: string }
37
+ ```
38
+
39
+ E o `send` do `<Chat>` vira uma união — **o contrato atual é o caso degenerado**:
40
+
41
+ ```ts
42
+ type SendResult = Promise<string> | AsyncIterable<ChatEvent>
43
+ // Promise<string> ≡ [{ type: 'text', delta: s }, { type: 'done', ok: true }]
44
+ ```
45
+
46
+ ## Decisões de desenho (e porquês)
47
+
48
+ 1. **`tool` cru, humanização no componente.** Maestro e Vetra BI humanizam o nome
49
+ ("Lendo o projeto…"); é apresentação, não dado. O `<Chat>` traz o `humanizeTool`
50
+ default (pt-BR, o do ChatRail) e aceita override por prop. O protocolo não opina.
51
+ 2. **Tool NÃO entra no transcript.** Convenção provada nos dois consumidores: eventos
52
+ `tool` alimentam o indicador vivo (pontinhos + rótulo), não viram mensagem. O
53
+ transcript é o que foi dito; a atividade é estado transiente.
54
+ 3. **`artifact` é aberto e o render é do app.** O componente não conhece "relatório
55
+ Evidence" — recebe `renderArtifact?: (a) => ReactNode` e um fallback (link). O card
56
+ com iframe da Vetra BI é um `renderArtifact` do app.
57
+ 4. **Transporte fora do contrato.** SSE, fetch stream, WebSocket: problema do app —
58
+ o `send` devolve o iterável e pronto. Em particular, **replay/reconexão continua do
59
+ transporte** (a lição do ChatRail — replay idempotente, buffer até `synced` — vive na
60
+ camada SSE do app; o `<Chat>` recebe `history` pronto + eventos do turno vivo).
61
+ Sobe pro componente só se um segundo caso real cobrar.
62
+ 5. **A camada ai ganha a variante streaming, aditiva.** `run()` permanece;
63
+ `runStream(input, opts): AsyncIterable<ChatEvent>` mapeia o loop agêntico no
64
+ protocolo (tool call → `tool`; texto → `text`; confirmação destrutiva segue o
65
+ contrato atual). O `chatRoute` canônico da doc passa a poder streamar (SSE) e o
66
+ `<Chat>` consumi-lo — o Copilot migra quando quiser, sem quebrar.
67
+ 6. **Nada quebra.** `send` atual compila e funciona igual (união). O `<Chat>` novo é o
68
+ mesmo componente com mais um modo, não um componente novo.
69
+
70
+ ## Não-objetivos (por ora)
71
+
72
+ Memória longa/resumo de conversa, multi-agente na mesma sala, persistência de
73
+ transcript no componente, replay no componente — sem caso real maduro; entram pela
74
+ régua quando cobrarem.
75
+
76
+ ## Mapa de migração (depois do contrato pronto)
77
+
78
+ | De | Para |
79
+ |---|---|
80
+ | `bi-poc/app/components/chat-rail.tsx` | `<Chat send={streamTurn} renderArtifact={ReportCard} …/>` |
81
+ | ChatRail do Maestro (App.tsx ~1376) | idem, mantendo a camada SSE/replay do app como transporte |
82
+ | Copilot | inalterado; opcionalmente `runStream` no backend quando quiser streaming |
83
+
84
+ Doadores de implementação: indicador/humanização e disciplina de replay do ChatRail;
85
+ `renderArtifact` e o mapeamento de eventos da Vetra BI.
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: Code style
3
+ order: 5
4
+ ---
5
+
6
+ # Code style — padrão softize
7
+
8
+ > **A fonte desta regra migrou pro admin** (metodologia 12/06/2026): skill `code-style`,
9
+ > vinculada a todos os papéis e materializada em `.claude/skills/code-style/SKILL.md`
10
+ > via `maestro pull`. Edite LÁ (aba Skills do admin) — este arquivo é só o ponteiro.
11
+
12
+ Resumo de uma linha: **código em inglês; o que humano lê (UI, comentário, doc) em pt-BR;
13
+ frases com maiúscula e ponto; labels curtos sem ponto.**
14
+
15
+ O que dá, é regra executável no `opus check` (naming `<resource>.<verb>`, ordem dos
16
+ campos do spec, registro no runtime); o resto vive na skill.
@@ -0,0 +1,246 @@
1
+ ---
2
+ title: Camada de dados
3
+ order: 2
4
+ ---
5
+
6
+ # Camada de dados — decisão e contrato
7
+
8
+ > Decisão (jun/2026). Fecha uma deliberação longa sobre engine de DB, fonte do
9
+ > schema e migrations. Doc em pt-BR. Complementa a §15 (Entidades) do
10
+ > `protocol.md`.
11
+
12
+ ## Decisão
13
+
14
+ | Eixo | Escolha |
15
+ |---|---|
16
+ | **Engine de query** | **Kysely** (multi-banco, MSSQL incluso, já em prod no `admin/api`) |
17
+ | **Fonte do schema** | **`defineEntity`** (única; ver §15) |
18
+ | **Rodar migration** | **`Migrator` nativo do Kysely** |
19
+ | **Escrever migration** | **`up/down` à mão** (scaffold de rascunho opcional) |
20
+ | **Garantir sync** | **drift-check** — comparador read-only sobre a introspecção do Kysely |
21
+ | **Auto-gen de migration** | **opt-in futuro** (drizzle-kit ou Atlas), mesmo `entityColumns` |
22
+
23
+ **Stack = Kysely + a camada opus. Zero tooling estrangeiro, uma dep só (`kysely`).**
24
+
25
+ ## Por que não…
26
+
27
+ - **Drizzle como engine** — ele quer **ser** o schema (a `pgTable`); isso cede a
28
+ fonte única (perde o tipo lógico que dirige UI/validação/OpenAPI/DB de uma só
29
+ declaração) e o swap de engine. O `defineEntity` fica por cima como declaração;
30
+ Drizzle no máximo seria um *materializador* opt-in.
31
+ - **Differ próprio** (introspecção → ALTERs) — é **possuir um engine de migration**
32
+ (contra o §9). A gente possui só um **comparador** read-only, que é trivial e
33
+ seguro perto de gerar ALTERs corretos.
34
+ - **drizzle-kit / Atlas como base** — drizzle-kit arrasta uma 2ª representação de
35
+ schema (a `pgTable`, mesmo gerada) + dep `drizzle-orm`; Atlas é binário Go no
36
+ toolchain. Viram **aceleradores opt-in** (mesmo `entityColumns` de entrada),
37
+ não a base.
38
+
39
+ ## O contrato
40
+
41
+ ```
42
+ defineEntity ──► entityColumns(entity) # colunas resolvidas (id auto, timestamps, deletedAt)
43
+
44
+ ├─► desiredColumns(entity) # shape normalizado pro diff
45
+
46
+ db (Kysely) ──► db.introspection.getTables() # shape real do banco
47
+
48
+ └─► diffColumns(table, desired, actual) ─► DriftFinding[]
49
+
50
+ kyselyDriftCheck(db, entities) # cola tudo; roda no `opus check`
51
+ ```
52
+
53
+ - **`entityColumns(entity)`** (já existe) — colunas resolvidas + injetadas.
54
+ - **`desiredColumns(entity)`** — `{ name, nullable }[]` derivado de `entityColumns`.
55
+ - **`diffColumns(table, desired, actual)`** — **puro**, sem Kysely. Retorna
56
+ `DriftFinding[]` (`missing_table`, `missing_column`, `extra_column`,
57
+ `nullable_mismatch`). É o núcleo testável.
58
+ - **`kyselyDriftCheck(db, entities)`** — bind fino: introspecta via Kysely e roda
59
+ o differ puro por tabela.
60
+
61
+ ## CLI: grupo `db` (separado do `opus check`)
62
+
63
+ O `opus check` é **estático puro** (source-only, sem banco). O drift-check precisa
64
+ de conexão + entidades carregadas — natureza diferente. Então vive num **grupo
65
+ próprio**, `opus db <verbo>` (não estufa o `check`):
66
+
67
+ ```
68
+ opus db check # drift-check (entidade ↔ banco) — read-only, exit ≠ 0 se divergir
69
+ opus db migrate # aplica o SCHEMA IDEMPOTENTE + drift-check na sequência
70
+ opus db scaffold # gera rascunho a partir do diff (referência pro schema)
71
+ ```
72
+
73
+ **Schema idempotente evolutivo** (o padrão da casa, não migration versionada): UM
74
+ script SQL (`config.schema`, ex. `src/db/schema.sql`) re-rodável — `CREATE IF NOT
75
+ EXISTS` + guards `DO $$ IF EXISTS` cobrem nascer do zero E upgrade de prod no mesmo
76
+ artefato. Não existe `migrate down`: rollback = editar o script e re-rodar
77
+ (catástrofe = snapshot pré-deploy). Upgrades já absorvidos por todos os ambientes
78
+ podem ser podados do script. Depois de aplicar, o `migrate` roda o drift-check na
79
+ mesma conexão — migrou mas diverge das entidades é estado quebrado que o deploy
80
+ precisa ver na hora (exit ≠ 0). (A era Kysely Migrator foi aposentada: DDL `.ts`
81
+ gerava identificadores camelCase errados e o formato nunca pegou.)
82
+
83
+ O `db scaffold` roda o drift-check e emite `migrations/<ts>_scaffold.ts`: gera os
84
+ casos **limpos** (tabela/coluna nova) e sinaliza como `// TODO` os **arriscados**
85
+ (nullable/drop, dialect-específicos). É **referência** pra escrever o SQL no
86
+ schema — a verdade é o script; revise à mão.
87
+
88
+ Os comandos `db` carregam o `opus.config.ts` via `tsx` num runner isolado (mesma
89
+ infra do `gen`, não a do `check`). Contrato do config **pros comandos `db`**:
90
+
91
+ ```ts
92
+ export default {
93
+ domains: [...], // entidades coletadas de domain.models
94
+ entities: [Deal, Company], // ou explícitas (opcional)
95
+ schema: './src/db/schema.sql', // o script idempotente (default: ./db/schema.sql)
96
+ database: () => new Kysely({ ... }), // factory LAZY do banco
97
+ }
98
+ ```
99
+
100
+ `database` é **factory, não instância**, de propósito: o `gen` importa o mesmo
101
+ config e **nunca chama** `database()`, então não abre conexão. Só os comandos `db`
102
+ chamam. CI compõe: `opus check && opus db check`.
103
+
104
+ ## Fluxo do dev
105
+
106
+ 1. Escreve/edita a entidade (`defineEntity`).
107
+ 2. Evolui o **schema idempotente** (SQL, com guards) — o scaffold rascunha o diff
108
+ como referência quando ajuda.
109
+ 3. `opus db migrate` aplica e já cobra o **drift-check**: se o banco divergir do
110
+ `defineEntity`, **falha** — o script e a entidade nunca silenciam um drift.
111
+
112
+ ## Uso ponta a ponta
113
+
114
+ ### 1. Entidade
115
+ ```ts
116
+ // src/domains/deals/deal.entity.ts
117
+ import { defineEntity, belongsTo } from '@softize/opus/schema'
118
+ import { t } from '@softize/opus/schema/zod'
119
+
120
+ export const Deal = defineEntity({
121
+ name: 'deal',
122
+ fields: {
123
+ title: t.string(),
124
+ amount: t.money(),
125
+ status: t.enum(['open', 'won', 'lost']).default('open'),
126
+ ownerId: t.uuid(),
127
+ notes: t.text().nullable(),
128
+ },
129
+ relations: { owner: belongsTo('user', { from: 'ownerId' }) },
130
+ timestamps: true,
131
+ softDelete: true,
132
+ })
133
+ ```
134
+
135
+ ### 2. Config (uma vez)
136
+ ```ts
137
+ // opus.config.ts
138
+ import { Kysely, PostgresDialect, CamelCasePlugin } from 'kysely'
139
+ import { Deal } from './src/domains/deals/deal.entity.ts'
140
+
141
+ export default {
142
+ entities: [Deal], // ou domains: [...] (entidades via domain.models)
143
+ naming: 'snake', // colunas snake no banco
144
+ migrations: './migrations',
145
+ database: () => new Kysely({
146
+ dialect: new PostgresDialect({ pool }),
147
+ plugins: [new CamelCasePlugin()], // backend camel ↔ banco snake
148
+ }),
149
+ }
150
+ ```
151
+
152
+ ### 3. Schema
153
+ ```bash
154
+ opus db scaffold # rascunha o diff (referência pro SQL — revise!)
155
+ opus db migrate # aplica o schema idempotente + drift-check
156
+ opus db check # gate read-only: banco == entidade
157
+ ```
158
+
159
+ ### 4. CRUD instantâneo
160
+ ```ts
161
+ import { crudActions } from '@softize/opus/data/kysely'
162
+
163
+ // gera deal.view/create/update/delete/search (só as ops com authorize)
164
+ export const dealActions = crudActions(Deal, {
165
+ view: (ctx) => ctx.can('deal:read'),
166
+ create: (ctx) => ctx.can('deal:create'),
167
+ update: (ctx) => ctx.can('deal:update'),
168
+ delete: (ctx) => ctx.can('deal:delete'),
169
+ search: (ctx) => ctx.can('deal:read'),
170
+ })
171
+ // runtime.register(Object.values(dealActions))
172
+ ```
173
+
174
+ ### 5. Handler com lógica própria
175
+ ```ts
176
+ import { defineAction } from '@softize/opus'
177
+ import { kyselyRepo } from '@softize/opus/data/kysely'
178
+
179
+ defineAction({
180
+ name: 'deal.win', kind: 'simple',
181
+ input: z.object({ id: t.uuid().zod() }),
182
+ output: entityRowSchema(Deal),
183
+ authorize: (ctx) => ctx.can('deal:update'),
184
+ handler: (ctx, input) => kyselyRepo(ctx.db, Deal).update(input.id, { status: 'won' }),
185
+ })
186
+ ```
187
+
188
+ ## Naming (camel ↔ snake)
189
+
190
+ O backend é **100% camelCase** (TS idiomático); o snake_case mora só no banco. Quem
191
+ faz a ponte é o **`CamelCasePlugin` do Kysely** — ele reescreve query e DDL
192
+ (`ownerId` → `owner_id`) na ida e os resultados na volta. Repo/scaffold/crud do Opus
193
+ escrevem camel e **não tocam no assunto**.
194
+
195
+ A exceção é a **introspecção** (`db.introspection.getTables()`): ela volta os nomes
196
+ **crus** (snake), fora do plugin. Por isso o **drift-check** é o único ponto que o
197
+ opus mapeia — ele aplica a `naming` no schema desejado antes de comparar.
198
+
199
+ ```ts
200
+ // opus.config.ts
201
+ export default {
202
+ naming: 'snake', // default; 'identity' | fn custom
203
+ database: () => new Kysely({
204
+ dialect,
205
+ plugins: [new CamelCasePlugin()], // OBRIGATÓRIO p/ naming: 'snake'
206
+ }),
207
+ }
208
+ ```
209
+
210
+ > **Footgun auto-denunciado:** se esquecer o `CamelCasePlugin`, o repo cria colunas
211
+ > camelCase; o drift-check (snake) não bate → `db check` **falha** e mostra o erro.
212
+ > A consistência é garantida pelo gate, não pela disciplina.
213
+
214
+ ## Limitações conhecidas (refinar depois)
215
+
216
+ - **Nível-coluna:** o drift-check compara **presença + nullable**. Tipos, defaults,
217
+ índices, FKs e check constraints ficam pra um passo 2 (queries `pg_catalog` via
218
+ `sql` do Kysely — ainda tudo-JS).
219
+ - **Drift de índices/FK** ainda fora (só colunas).
220
+
221
+ ## Sequência de construção
222
+
223
+ 1. ✅ `entityColumns` (§15).
224
+ 2. ✅ `diffColumns` (puro) + `desiredColumns` + `kyselyDriftCheck`.
225
+ 3. ✅ Grupo CLI `db` + `opus db check` (carrega config via tsx, factory `database()`).
226
+ 4. ✅ `kyselyRepo(db, entity)` — CRUD tipado (insert/findById/update/remove/list),
227
+ gera id (pk auto), preenche timestamps, respeita soft-delete. Falta o açúcar
228
+ `ctx.repo(Entity)` (wiring na ActionContext).
229
+ 5. ✅ `opus db migrate` — schema idempotente evolutivo (SQL, config `schema`) + drift-check
230
+ embutido. (A 1ª versão era Kysely Migrator; aposentada — o formato nunca pegou.)
231
+ 6. ✅ `opus db scaffold` (rascunho `up/down` do diff; casos limpos gerados, riscos viram `// TODO`).
232
+ 7. ✅ Composição (Opção D — sem tocar o core): schema builders
233
+ (`entityRowSchema`/`entityInsertSchema`/`entityUpdateSchema`) + `crudActions(entity,
234
+ {authorize})` no driver Kysely. Decisão: `entity:` no `defineAction` e `ctx.repo`
235
+ no `ActionContext` puxariam tipos de entidade pro core (ciclo schema↔core) — então
236
+ a composição vive onde core+schema convivem; usa-se `crudActions` / `kyselyRepo(ctx.db, E)`.
237
+ 8. ✅ `crudActions` + `kyselyRepo` para `search` (paginação offset + filtros de igualdade →
238
+ `Paginated`). CRUD completo: view/create/update/delete/search.
239
+ 9. ✅ `NamingStrategy` (camel↔snake) — **decisão: `CamelCasePlugin` do Kysely é dono do
240
+ mapeamento de query/DDL** (backend 100% camel; snake só no banco). O Opus snakeia
241
+ **só o drift-check** (a introspecção volta crua, fora do plugin). Default `snake`;
242
+ config em `opus.config.ts` (`naming`). Repo/scaffold/crud ficam camel, intocados.
243
+ Esquecer o plugin → o `db check` falha e expõe (footgun auto-denunciado).
244
+ 10. Aprofundar o drift (índices/FK/tipos/defaults).
245
+ 11. Cursor pagination + `FilterSpec` ricos no search (hoje offset + igualdade).
246
+ 12. (Opt-in) drivers de auto-gen: drizzle-kit / Atlas.
@@ -0,0 +1,102 @@
1
+ # Ownership dos componentes: "it's ours" POR CONTATO (decidido)
2
+
3
+ > **Status: DECIDIDO (João, jul/2026).** O modelo é o "afrouxar" da seção final, com uma
4
+ > regra simples de transição: **tocar = assumir**.
5
+
6
+ ## A decisão
7
+
8
+ 1. **Tocar um componente quebra o vínculo — e tudo bem.** Qualquer opinião da casa num
9
+ componente `locked` o ejeta sem cerimônia nem culpa: vira `ejected` (nosso), com o
10
+ delta documentado no lock. É o caminho abençoado, não a exceção.
11
+ 2. **A referência ao original FICA.** `source` + `upstreamHash` permanecem no lock como
12
+ memória de origem — pra IA garimpar o shadcn quando valer (merge de 3 vias sob
13
+ demanda: base gravada → shadcn atual → nosso). Referência, não contrato.
14
+ 3. **Componente nunca tocado segue "isn't ours" por ora.** `locked` + hash-gate ativos
15
+ até o primeiro toque — o QA de graça do shadcn continua valendo onde a casa ainda não
16
+ tem opinião.
17
+ 4. Com o tempo, a biblioteca inteira converge pra "ours" naturalmente, pelo uso — sem
18
+ big-bang, sem cerimônia de virada.
19
+
20
+ ## Princípio irmão: sobrescrita sancionada
21
+
22
+ Sobrescrever um default do Tailwind/shadcn é legítimo quando **declarado** num dos dois
23
+ registros — o header de DIVERGÊNCIAS do `theme.css` (valores de token) ou o delta do
24
+ `registry.lock.json` (componentes ejetados). Nunca sobrescrever a **mecânica** (utilitário
25
+ mantém o significado documentado; preferir namespace próprio, como a elevação
26
+ `shadow-card/popover/dialog`). O que não está declarado é drift.
27
+
28
+ ---
29
+
30
+ *O histórico abaixo é o contexto que levou à decisão.*
31
+
32
+ ## O que está em jogo
33
+
34
+ Hoje o `opus/ui` é **shadcn-native**: cada primitivo é byte-fiel ao registry do shadcn,
35
+ com hash no `registry.lock.json` e um gate (`tests/ui/lock.test.ts`) que reprova se
36
+ alguém editar um `locked` à mão. Customização da casa vai **por fora** (theme/cva/
37
+ wrapper) ou o componente é **ejected** (assumido, delta documentado, fora do hash-check).
38
+
39
+ A proposta é **inverter o default**: os componentes passam a ser **nossos** por padrão;
40
+ o shadcn vira a **origem documentada** e uma **referência** de onde puxar melhorias quando
41
+ valer — não um contrato de identidade byte-a-byte.
42
+
43
+ ## O gatilho (por que isto veio à tona)
44
+
45
+ Ao deixar os controles "flat" (sem `shadow-xs`), a primeira tentativa foi neutralizar o
46
+ token no tema: `--shadow-xs: 0 0 #0000`. Isso **mente sobre o que a variável contém** —
47
+ um token de escala redefinido pra "nada" — só pra **não editar 16 componentes locked**.
48
+ O lock empurrou a solução pra uma desonestidade. Corrigido na 2.20.1 (a sombra saiu de
49
+ dentro dos componentes; os 14 byte-fiéis viraram `ejected`).
50
+
51
+ A lição, do João: **honestidade da abstração > conveniência de sync.** Mentir num token
52
+ é pior do que desvincular componentes. O sync é conveniência de menor prioridade.
53
+
54
+ ## O que o lock compra e custa
55
+
56
+ **Compra:** QA de graça (a11y/teclado/Radix do shadcn), disciplina anti-drift, proveniência
57
+ ("nossos primitivos SÃO o shadcn"), re-sync barato por hash-diff.
58
+
59
+ **Custa:** imposto interpretativo em toda opinião da casa ("posso tocar isso? theme? cva?
60
+ ejetar?") — e, como o episódio da sombra mostrou, empurra pra contorções desonestas pra
61
+ respeitar o lock. Um design system existe pelas suas opiniões; alugar os primitivos de um
62
+ upstream com quem se tem que ficar idêntico atrita com ter opinião.
63
+
64
+ ## A proposta
65
+
66
+ 1. **Dropar o contrato de hash.** O `lock.test` deixa de reprovar edição; no máximo vira
67
+ advisory. Componentes são editados livremente, com honestidade (a intenção mora no
68
+ componente, não num token esvaziado).
69
+ 2. **O `registry.lock.json` vira ledger de proveniência.** Mantém `upstreamHash` /
70
+ `source` (qual versão do shadcn cada um forkou = o **merge-base**). Deixa de ser gate,
71
+ vira memória de origem.
72
+ 3. **Re-sync por diff sob demanda, com IA.** Em vez de "re-porta e o hash diz o que
73
+ mudou", o fluxo é um **merge de 3 vias**: base (o `upstreamHash` gravado) → shadcn
74
+ atual → nosso customizado. O Claude lê os dois diffs e reconcilia preservando o delta
75
+ da casa. Vale uma skill/comando (`opus resync <componente>`) pra ser um gesto de uma
76
+ linha, com teste + revisão como rede de segurança (o hash determinístico sai, o
77
+ julgamento entra).
78
+
79
+ ## Tradeoffs honestos
80
+
81
+ **A favor:** honestidade (sem mentira-de-token, sem contorção); liberdade de opinião sem
82
+ pedir licença; o `opus/ui` deixa de ser "shadcn com theme" e vira design system próprio de
83
+ verdade. E o re-sync não morre — o merge-de-IA cobre a lacuna que o hash cobria.
84
+
85
+ **Contra:** a casa passa a **owar a superfície de manutenção** de ~44 componentes (a11y,
86
+ segurança, bumps de Radix) — o shadcn fazia esse trabalho chato de graça. O re-sync vira
87
+ julgamento (merge de IA), não determinismo (hash) → depende de **teste + revisão** pegarem
88
+ um merge torto, e de **alguém iniciar** o pull (não vem passivo pelo CI).
89
+
90
+ ## A pergunta que travava a decisão
91
+
92
+ **O time (João + agentes) tem banda pra manter os primitivos?** A resposta escolhida foi o
93
+ meio honesto: **afrouxar** em vez de matar — ejeção trivial e abençoada pra qualquer
94
+ opinião (matando a tentação da mentira-de-token), hash mantido só nos que ninguém
95
+ customizou. É exatamente a decisão do topo.
96
+
97
+ ## O que já mudou (2.20.1)
98
+
99
+ Os 14 controles que usavam `shadow-xs` foram ejetados (flat mora dentro deles agora);
100
+ `ejected` foi de 7 pra 21, `locked` de 44 pra 30. **Isto é a primeira instância concreta
101
+ do "it's ours"** — mas foi escopada só aos controles com `shadow-xs`. A decisão desta
102
+ proposta é se a gente estende isso pra biblioteca inteira e formaliza o modelo acima.