@softize/opus 12.6.3 → 12.7.1

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 (35) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +20 -2
  3. package/bin/cli.mjs +10 -1
  4. package/bin/lib/gen-manifest.mjs +1 -0
  5. package/bin/lib/gen-runner.mjs +29 -1
  6. package/bin/lib/seed-runner.mjs +152 -0
  7. package/bin/lib/seed.mjs +229 -0
  8. package/docs/adr/0002-structured-seeds-are-declared-and-bound.md +130 -0
  9. package/docs/protocol.md +10 -1
  10. package/docs/seeds.md +132 -0
  11. package/package.json +5 -1
  12. package/registry/instructions/opus.md +4 -0
  13. package/registry/skills/apply-opus-seed/SKILL.md +45 -0
  14. package/registry/skills/apply-opus-seed/agents/openai.yaml +4 -0
  15. package/registry/skills/apply-opus-seed/references/evaluations.md +8 -0
  16. package/registry/skills/create-opus-seed/SKILL.md +55 -0
  17. package/registry/skills/create-opus-seed/agents/openai.yaml +4 -0
  18. package/registry/skills/create-opus-seed/references/contract.md +16 -0
  19. package/registry/skills/create-opus-seed/references/evaluations.md +8 -0
  20. package/registry/skills/create-opus-seed/scripts/scaffold.mjs +78 -0
  21. package/registry/skills/implement-opus-change/SKILL.md +3 -1
  22. package/src/core/index.ts +2 -0
  23. package/src/core/types.ts +16 -1
  24. package/src/seed/index.ts +391 -0
  25. package/src/ui/components/patterns/form.tsx +131 -19
  26. package/src/ui/components/primitives/detail.tsx +113 -0
  27. package/src/ui/components/primitives/tabs.tsx +1 -1
  28. package/src/ui/docs/content/action-form.md +10 -0
  29. package/src/ui/docs/content/cli.md +17 -0
  30. package/src/ui/docs/content/detail.md +38 -0
  31. package/src/ui/docs/registry.tsx +2 -0
  32. package/src/ui/drivers/react.tsx +3 -2
  33. package/src/ui/lib/object-schema.ts +36 -0
  34. package/src/ui/meta.ts +7 -1
  35. 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 }
@@ -80,7 +80,7 @@ function TabsTrigger({
80
80
  className={cn(
81
81
  "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
82
82
  "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
83
- "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
83
+ "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:text-foreground",
84
84
  "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-1px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
85
85
  className
86
86
  )}
@@ -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
+ ```
@@ -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) },
@@ -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 as { shape?: Record<string, unknown> } | null)?.shape
331
- if (shape === undefined || shape === null) return values
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. Pra alternar a visão de uma seção ou montar uma barra de formatação. Pra escolha única num form com rótulos longos, RadioGroup.',
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,