@softize/opus 12.6.2 → 12.7.0
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 +26 -1
- package/README.md +20 -2
- package/bin/cli.mjs +10 -1
- package/bin/lib/gen-manifest.mjs +1 -0
- package/bin/lib/gen-runner.mjs +29 -1
- package/bin/lib/seed-runner.mjs +152 -0
- package/bin/lib/seed.mjs +229 -0
- package/docs/adr/0002-structured-seeds-are-declared-and-bound.md +130 -0
- package/docs/protocol.md +10 -1
- package/docs/seeds.md +132 -0
- package/package.json +5 -1
- package/registry/instructions/opus.md +4 -0
- package/registry/skills/apply-opus-seed/SKILL.md +45 -0
- package/registry/skills/apply-opus-seed/agents/openai.yaml +4 -0
- package/registry/skills/apply-opus-seed/references/evaluations.md +8 -0
- package/registry/skills/build-opus-ui/SKILL.md +12 -6
- package/registry/skills/build-opus-ui/references/evaluations.md +8 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +26 -0
- package/registry/skills/create-opus-seed/SKILL.md +55 -0
- package/registry/skills/create-opus-seed/agents/openai.yaml +4 -0
- package/registry/skills/create-opus-seed/references/contract.md +16 -0
- package/registry/skills/create-opus-seed/references/evaluations.md +8 -0
- package/registry/skills/create-opus-seed/scripts/scaffold.mjs +78 -0
- package/registry/skills/implement-opus-change/SKILL.md +3 -1
- package/src/core/index.ts +2 -0
- package/src/core/types.ts +16 -1
- package/src/seed/index.ts +391 -0
- package/src/ui/components/patterns/form.tsx +131 -19
- package/src/ui/components/primitives/detail.tsx +113 -0
- package/src/ui/docs/content/action-form.md +10 -0
- package/src/ui/docs/content/cli.md +17 -0
- package/src/ui/docs/content/detail.md +38 -0
- package/src/ui/docs/registry.tsx +2 -0
- package/src/ui/drivers/react.tsx +3 -2
- package/src/ui/lib/object-schema.ts +36 -0
- package/src/ui/meta.ts +7 -1
- package/src/ui/react.tsx +9 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/cn.ts'
|
|
4
|
+
|
|
5
|
+
export type DetailGroupVariant = 'plain' | 'framed'
|
|
6
|
+
export type DetailGroupOrientation = 'vertical' | 'horizontal'
|
|
7
|
+
export type DetailGroupColumns = 1 | 2 | 3 | 4 | 'auto'
|
|
8
|
+
|
|
9
|
+
export interface DetailGroupProps extends React.ComponentProps<'dl'> {
|
|
10
|
+
/** `framed` aplica a superfície e a moldura canônicas ao conjunto. */
|
|
11
|
+
variant?: DetailGroupVariant
|
|
12
|
+
/** Exibe hairlines somente entre os campos, sem exigir moldura externa. */
|
|
13
|
+
dividers?: boolean
|
|
14
|
+
/** Número responsivo de colunas ou distribuição automática por largura mínima. */
|
|
15
|
+
columns?: DetailGroupColumns
|
|
16
|
+
/** Organiza a chave sobre o valor ou ao lado dele em cada campo. */
|
|
17
|
+
orientation?: DetailGroupOrientation
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const columnClasses: Record<DetailGroupColumns, string> = {
|
|
21
|
+
1: 'grid-cols-1',
|
|
22
|
+
2: 'grid-cols-1 sm:grid-cols-2',
|
|
23
|
+
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
|
24
|
+
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
|
25
|
+
auto: 'grid-cols-[repeat(auto-fit,minmax(min(100%,14rem),1fr))]',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function DetailGroup({
|
|
29
|
+
className,
|
|
30
|
+
variant = 'plain',
|
|
31
|
+
dividers = false,
|
|
32
|
+
columns = 1,
|
|
33
|
+
orientation = 'vertical',
|
|
34
|
+
...props
|
|
35
|
+
}: DetailGroupProps): React.ReactElement {
|
|
36
|
+
return (
|
|
37
|
+
<dl
|
|
38
|
+
data-slot="detail-group"
|
|
39
|
+
data-variant={variant}
|
|
40
|
+
data-dividers={dividers}
|
|
41
|
+
data-orientation={orientation}
|
|
42
|
+
className={cn(
|
|
43
|
+
'group/detail-group grid min-w-0',
|
|
44
|
+
columnClasses[columns],
|
|
45
|
+
variant === 'plain' && !dividers && 'gap-x-6 gap-y-4',
|
|
46
|
+
dividers && 'gap-px overflow-hidden',
|
|
47
|
+
(variant === 'framed' || dividers) &&
|
|
48
|
+
'[&>[data-slot=detail-field]]:p-3',
|
|
49
|
+
variant === 'framed' &&
|
|
50
|
+
'overflow-hidden rounded-lg border border-border bg-card text-card-foreground',
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface DetailFieldProps extends Omit<
|
|
59
|
+
React.ComponentProps<'div'>,
|
|
60
|
+
'children'
|
|
61
|
+
> {
|
|
62
|
+
label: React.ReactNode
|
|
63
|
+
value: React.ReactNode
|
|
64
|
+
/** Ícone decorativo que identifica o campo antes do par chave/valor. */
|
|
65
|
+
icon?: React.ReactNode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function DetailField({
|
|
69
|
+
className,
|
|
70
|
+
label,
|
|
71
|
+
value,
|
|
72
|
+
icon,
|
|
73
|
+
...props
|
|
74
|
+
}: DetailFieldProps): React.ReactElement {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="detail-field"
|
|
78
|
+
className={cn(
|
|
79
|
+
'grid min-w-0 items-start',
|
|
80
|
+
'group-data-[orientation=vertical]/detail-group:grid-cols-1 group-data-[orientation=horizontal]/detail-group:grid-cols-[minmax(0,auto)_minmax(0,1fr)]',
|
|
81
|
+
'group-data-[orientation=vertical]/detail-group:gap-y-1 group-data-[orientation=horizontal]/detail-group:items-baseline group-data-[orientation=horizontal]/detail-group:gap-x-3',
|
|
82
|
+
'group-data-[dividers=true]/detail-group:outline group-data-[dividers=true]/detail-group:outline-1 group-data-[dividers=true]/detail-group:outline-border',
|
|
83
|
+
'group-data-[variant=framed]/detail-group:bg-card group-data-[variant=framed]/detail-group:text-card-foreground',
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
<dt
|
|
89
|
+
data-slot="detail-field-label"
|
|
90
|
+
className="flex items-center gap-2.5 text-xs leading-snug font-medium text-muted-foreground group-data-[orientation=horizontal]/detail-group:text-sm"
|
|
91
|
+
>
|
|
92
|
+
{icon !== undefined && (
|
|
93
|
+
<span
|
|
94
|
+
data-slot="detail-field-icon"
|
|
95
|
+
aria-hidden="true"
|
|
96
|
+
className="flex size-4 shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4"
|
|
97
|
+
>
|
|
98
|
+
{icon}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
{label}
|
|
102
|
+
</dt>
|
|
103
|
+
<dd
|
|
104
|
+
data-slot="detail-field-value"
|
|
105
|
+
className="min-w-0 text-sm leading-snug font-normal"
|
|
106
|
+
>
|
|
107
|
+
{value}
|
|
108
|
+
</dd>
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { DetailGroup, DetailField }
|
|
@@ -93,6 +93,16 @@ ref }` resolve pelos dicts do `<TbdlibProvider dicts={{ ref: meuDict }}>` — o
|
|
|
93
93
|
resolve value→label pela meta que viaja no contrato, sem registry) > **chaves cruas do
|
|
94
94
|
z.enum**. Campo texto com opções declaradas (runtime ou spec) vira single-select por-id.
|
|
95
95
|
|
|
96
|
+
## Widgets declarativos
|
|
97
|
+
|
|
98
|
+
`FieldSpec.widget` aceita `textarea`, `code`, `lines`, `refItems`, `icon` e `toggle-group`.
|
|
99
|
+
O literal canônico do grupo de escolhas é sempre kebab-case: `widget: 'toggle-group'`. Em enum ele
|
|
100
|
+
renderiza seleção única; em array de enum, seleção múltipla. As opções seguem a mesma precedência e
|
|
101
|
+
o mesmo `SelectOption` rico dos selects. Use esse widget quando cada opção funcionar como uma
|
|
102
|
+
escolha rica em card, com conteúdo de apoio; para uma lista textual comum, prefira `RadioGroup`.
|
|
103
|
+
Outros identificadores continuam válidos como metadado
|
|
104
|
+
para renderers próprios; o `ActionForm` aplica sua inferência normal quando não reconhece o widget.
|
|
105
|
+
|
|
96
106
|
## Controle custom: useActionFormContext
|
|
97
107
|
|
|
98
108
|
Campo com UI própria (grade de permissões, canvas…) que nenhum widget cobre? No modo
|
|
@@ -16,8 +16,25 @@ valida as convenções e expõe o estado vivo pros agentes via MCP.
|
|
|
16
16
|
opus check src # valida as convenções das actions (exit ≠ 0 se violar)
|
|
17
17
|
opus db check # drift entidade ↔ banco (read-only)
|
|
18
18
|
opus db migrate # aplica o schema idempotente + drift-check na sequência
|
|
19
|
+
opus seed check # valida bindings, dependências, ciclos e comandos paralelos
|
|
19
20
|
```
|
|
20
21
|
|
|
22
|
+
## Seeds de desenvolvimento e teste
|
|
23
|
+
|
|
24
|
+
> Seeds registrados no `opus.config.ts` têm perfis, métricas e escopos explícitos. Listagem e gate
|
|
25
|
+
> não abrem o banco; planejamento, aplicação e verificação são bloqueados em produção.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
opus seed list
|
|
29
|
+
opus seed plan customers.scenarios --profile smoke --scope local
|
|
30
|
+
opus seed apply customers.scenarios --profile smoke --scope local
|
|
31
|
+
opus seed verify customers.scenarios --profile smoke --scope local
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`apply` converge quando repetido; não há reset ou truncate no contrato. A skill
|
|
35
|
+
`$create-opus-seed` estrutura um novo dataset, e `$apply-opus-seed` opera um seed registrado pela
|
|
36
|
+
mesma CLI.
|
|
37
|
+
|
|
21
38
|
## Geração e introspecção
|
|
22
39
|
|
|
23
40
|
> As declarações (`description` de entidades/actions) são a fonte; o `gen` as projeta. O manifest
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
DetailGroup organiza dados somente leitura como uma lista semântica de pares chave/valor.
|
|
2
|
+
DetailField representa cada par e aceita conteúdo React rico em `label` e `value`. Use Field
|
|
3
|
+
para entrada e validação; use DetailField quando a pessoa apenas consulta um valor.
|
|
4
|
+
|
|
5
|
+
```tsx preview col
|
|
6
|
+
<DetailGroup columns={2}>
|
|
7
|
+
<DetailField label="Cliente" value="João Silva" />
|
|
8
|
+
<DetailField
|
|
9
|
+
label="Tipo"
|
|
10
|
+
value={<Badge variant="secondary">Prospect</Badge>}
|
|
11
|
+
/>
|
|
12
|
+
<DetailField
|
|
13
|
+
className="sm:col-span-2"
|
|
14
|
+
label="E-mail"
|
|
15
|
+
value="joao.silva@example.com"
|
|
16
|
+
icon={<Mail />}
|
|
17
|
+
/>
|
|
18
|
+
</DetailGroup>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Moldura e divisórias internas
|
|
22
|
+
|
|
23
|
+
`variant="framed"` adiciona a superfície e a borda externa. `dividers` desenha apenas as
|
|
24
|
+
divisórias internas; as duas opções são independentes e podem ser combinadas. `orientation`
|
|
25
|
+
define se a chave fica sobre o valor ou ao lado dele.
|
|
26
|
+
|
|
27
|
+
```tsx preview col
|
|
28
|
+
<DetailGroup columns={2} orientation="horizontal" variant="framed" dividers>
|
|
29
|
+
<DetailField label="Documento" value="Sem documento" />
|
|
30
|
+
<DetailField label="Fontes" value="NBS e Followize" />
|
|
31
|
+
<DetailField
|
|
32
|
+
className="sm:col-span-2"
|
|
33
|
+
label="E-mail"
|
|
34
|
+
value="cliente@example.com"
|
|
35
|
+
icon={<Mail />}
|
|
36
|
+
/>
|
|
37
|
+
</DetailGroup>
|
|
38
|
+
```
|
package/src/ui/docs/registry.tsx
CHANGED
|
@@ -67,6 +67,7 @@ import commandMd from './content/command.md?raw'
|
|
|
67
67
|
import composerMd from './content/composer.md?raw'
|
|
68
68
|
import copyableMd from './content/copyable.md?raw'
|
|
69
69
|
import dialogMd from './content/dialog.md?raw'
|
|
70
|
+
import detailMd from './content/detail.md?raw'
|
|
70
71
|
import drawerMd from './content/drawer.md?raw'
|
|
71
72
|
import menuMd from './content/menu.md?raw'
|
|
72
73
|
import emptyMd from './content/empty.md?raw'
|
|
@@ -363,6 +364,7 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
363
364
|
{ slug: 'dock', title: 'Dock', render: comp('Dock', 'dock', dockMd) },
|
|
364
365
|
{ slug: 'carousel', title: 'Carousel', render: comp('Carousel', 'carousel', carouselMd) },
|
|
365
366
|
{ slug: 'copyable', title: 'Copyable', render: comp('Copyable', 'copyable', copyableMd) },
|
|
367
|
+
{ slug: 'detail', title: 'Detail', render: comp('Detail', 'detail', detailMd) },
|
|
366
368
|
{ slug: 'item', title: 'Item', render: comp('Item', 'item', itemMd) },
|
|
367
369
|
{ slug: 'kbd', title: 'Kbd', render: comp('Kbd', 'kbd', kbdMd) },
|
|
368
370
|
{ slug: 'markdown', title: 'Markdown', render: comp('Markdown', 'markdown', markdownMd) },
|
package/src/ui/drivers/react.tsx
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
idleLookup,
|
|
44
44
|
} from '../index.ts'
|
|
45
45
|
import { zodErrorMapPtBr } from '../lib/zod-pt-br.ts'
|
|
46
|
+
import { objectSchemaShape } from '../lib/object-schema.ts'
|
|
46
47
|
|
|
47
48
|
// =============================================================================
|
|
48
49
|
// Provider
|
|
@@ -327,8 +328,8 @@ export interface UseFormActionResult<TInput extends FieldValues, TData>
|
|
|
327
328
|
* passa reto, fail-open).
|
|
328
329
|
*/
|
|
329
330
|
function normalizeEmptyStrings(schema: unknown, values: Record<string, unknown>): Record<string, unknown> {
|
|
330
|
-
const shape = (schema
|
|
331
|
-
if (shape === undefined
|
|
331
|
+
const shape = objectSchemaShape(schema)
|
|
332
|
+
if (shape === undefined) return values
|
|
332
333
|
const out: Record<string, unknown> = { ...values }
|
|
333
334
|
for (const [key, value] of Object.entries(out)) {
|
|
334
335
|
if (value !== '') continue
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encontra o shape de um objeto Zod atravessando wrappers como refine/superRefine.
|
|
3
|
+
* A inspeção é estrutural para funcionar com as representações v3 e v4 sem
|
|
4
|
+
* acoplar o driver React a uma única classe concreta do Zod.
|
|
5
|
+
*/
|
|
6
|
+
export function objectSchemaShape(schema: unknown): Record<string, unknown> | undefined {
|
|
7
|
+
let current = schema
|
|
8
|
+
const visited = new Set<object>()
|
|
9
|
+
|
|
10
|
+
while (current !== null && typeof current === 'object' && !visited.has(current)) {
|
|
11
|
+
visited.add(current)
|
|
12
|
+
const node = current as {
|
|
13
|
+
shape?: unknown
|
|
14
|
+
_def?: {
|
|
15
|
+
shape?: unknown
|
|
16
|
+
schema?: unknown
|
|
17
|
+
innerType?: unknown
|
|
18
|
+
in?: unknown
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const directShape = node.shape
|
|
22
|
+
if (directShape !== null && typeof directShape === 'object' && !Array.isArray(directShape)) {
|
|
23
|
+
return directShape as Record<string, unknown>
|
|
24
|
+
}
|
|
25
|
+
const definedShape =
|
|
26
|
+
typeof node._def?.shape === 'function'
|
|
27
|
+
? (node._def.shape as () => unknown)()
|
|
28
|
+
: node._def?.shape
|
|
29
|
+
if (definedShape !== null && typeof definedShape === 'object' && !Array.isArray(definedShape)) {
|
|
30
|
+
return definedShape as Record<string, unknown>
|
|
31
|
+
}
|
|
32
|
+
current = node._def?.schema ?? node._def?.innerType ?? node._def?.in
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
package/src/ui/meta.ts
CHANGED
|
@@ -35,6 +35,12 @@ export const componentMeta = {
|
|
|
35
35
|
whenToUse:
|
|
36
36
|
'Sinal compacto de estado quando o contexto ou o nome acessível já explica o significado. `variant` segue a intenção semântica (default/secondary/destructive/outline/success/warning/info). Passe `label` quando a cor comunicar informação; sem label, o ponto é decorativo. Para texto visível, use Badge.',
|
|
37
37
|
},
|
|
38
|
+
'detail': {
|
|
39
|
+
name: 'detail',
|
|
40
|
+
ancestry: 'opus',
|
|
41
|
+
whenToUse:
|
|
42
|
+
'Dados somente leitura em pares chave/valor. Compõe DetailGroup > DetailField com semântica de lista de definições; `columns` controla a grade, `orientation` posiciona chave e valor, `variant="framed"` aplica moldura e `dividers` acrescenta somente as divisórias internas. Para entrada, validação e erro, use Field; para uma coleção pesquisável, use ActionList.',
|
|
43
|
+
},
|
|
38
44
|
'dock': {
|
|
39
45
|
name: 'dock',
|
|
40
46
|
ancestry: 'opus',
|
|
@@ -341,7 +347,7 @@ export const componentMeta = {
|
|
|
341
347
|
name: 'toggle-group',
|
|
342
348
|
ancestry: 'shadcn',
|
|
343
349
|
whenToUse:
|
|
344
|
-
'Grupo de botões de alternância (Radix). `type` single (um ativo, value: string) ou multiple (vários, value: string[]). Controlado por `value`/`onValueChange`. `variant` default|outline, `size` default|sm|lg e `spacing` descem pros itens via contexto.
|
|
350
|
+
'Grupo de botões de alternância (Radix). `type` single (um ativo, value: string) ou multiple (vários, value: string[]). Controlado por `value`/`onValueChange`. `variant` default|outline, `size` default|sm|lg e `spacing` descem pros itens via contexto. Serve para alternar a visão de uma seção, montar uma barra de formatação ou apresentar escolhas ricas em cards pelo `ActionForm` com `widget: toggle-group`. Para uma escolha textual comum em formulário, especialmente com rótulos longos sem conteúdo de apoio, prefira RadioGroup.',
|
|
345
351
|
},
|
|
346
352
|
|
|
347
353
|
'confirm': {
|
package/src/ui/react.tsx
CHANGED
|
@@ -47,6 +47,15 @@ export { Label } from './components/primitives/label.tsx'
|
|
|
47
47
|
export { Badge, badgeVariants } from './components/primitives/badge.tsx'
|
|
48
48
|
export type { BadgeProps } from './components/primitives/badge.tsx'
|
|
49
49
|
|
|
50
|
+
export { DetailField, DetailGroup } from './components/primitives/detail.tsx'
|
|
51
|
+
export type {
|
|
52
|
+
DetailFieldProps,
|
|
53
|
+
DetailGroupColumns,
|
|
54
|
+
DetailGroupOrientation,
|
|
55
|
+
DetailGroupProps,
|
|
56
|
+
DetailGroupVariant,
|
|
57
|
+
} from './components/primitives/detail.tsx'
|
|
58
|
+
|
|
50
59
|
export {
|
|
51
60
|
Card,
|
|
52
61
|
CardHeader,
|