@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.
- package/CHANGELOG.md +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- package/src/vite/index.ts +8 -0
package/bin/lib/mcp.mjs
ADDED
|
@@ -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.
|