@softize/opus 12.7.1 → 12.8.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/bin/lib/gen-dicts.mjs +11 -1
  3. package/bin/lib/gen-runner.mjs +8 -2
  4. package/bin/lib/materialize.mjs +5 -2
  5. package/docs/adr/0003-dictionary-presentation-is-declared.md +160 -0
  6. package/package.json +1 -1
  7. package/registry/skills/build-opus-ui/SKILL.md +31 -10
  8. package/registry/skills/build-opus-ui/references/evaluations.md +25 -0
  9. package/registry/skills/build-opus-ui/references/ui-patterns.md +85 -0
  10. package/registry/skills/implement-opus-change/SKILL.md +4 -3
  11. package/registry/skills/model-opus-dictionary/SKILL.md +76 -0
  12. package/registry/skills/model-opus-dictionary/agents/openai.yaml +4 -0
  13. package/registry/skills/model-opus-dictionary/references/evaluations.md +18 -0
  14. package/src/core/dictionary.ts +152 -0
  15. package/src/core/index.ts +18 -0
  16. package/src/core/types.ts +9 -1
  17. package/src/schema/drivers/zod.ts +46 -13
  18. package/src/ui/components/patterns/list.tsx +136 -51
  19. package/src/ui/components/primitives/badge.tsx +3 -0
  20. package/src/ui/components/primitives/detail.tsx +12 -1
  21. package/src/ui/components/primitives/dictionary-value.tsx +141 -0
  22. package/src/ui/components/primitives/empty-value.tsx +50 -0
  23. package/src/ui/components/primitives/pagination.tsx +86 -56
  24. package/src/ui/docs/content/action-list-dialog.md +1 -1
  25. package/src/ui/docs/content/action-list.md +34 -9
  26. package/src/ui/docs/content/badge.md +6 -3
  27. package/src/ui/docs/content/detail.md +19 -2
  28. package/src/ui/docs/content/dictionary-value.md +120 -0
  29. package/src/ui/docs/content/empty-value.md +48 -0
  30. package/src/ui/docs/content/pagination.md +54 -17
  31. package/src/ui/docs/doc-client.tsx +21 -4
  32. package/src/ui/docs/registry.tsx +6 -0
  33. package/src/ui/meta.ts +14 -2
  34. package/src/ui/react.tsx +6 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,73 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
7
7
  `opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
8
8
  mudança cobra do seu código.
9
9
 
10
+ ## 12.8.0 — 2026-09-02
11
+
12
+ A apresentação de um dicionário passa a ser declarada no próprio `t.dict`, e não inferida por
13
+ quem desenha a tela (ADR 0003). `t.dict(entries, { presentation })` aceita `classification`,
14
+ `status`, `stage` ou `plain`; cada entrada pode declarar `tone` (`neutral` · `info` · `success` ·
15
+ `warning` · `danger`), `icon` (nome do catálogo `iconPickerIcons`) e `description` (texto curto
16
+ para a pessoa, usado em tooltip). `doc` continua sendo o entendimento de negócio para manifest e
17
+ Lens. `presentation` inválido, `tone` inválido ou `tone` fora de status e estágio falham na
18
+ construção do dicionário. O manifest projeta
19
+ `presentation` ao lado de `doc`, e os stubs de `opus gen` preservam as opções.
20
+
21
+ `@softize/opus/ui/react` exporta `DictionaryValue`, o renderer compartilhado: classificação vira
22
+ `Badge` `outline`, status e estágio viram `Badge` tonal pelo `tone` da entrada (`neutral` sem tom,
23
+ nunca `outline`), `plain` ou sem papel vira texto. Ícone só quando declarado e presente no
24
+ catálogo; tooltip só quando `description` acrescenta ao rótulo; o texto está sempre presente.
25
+ Overrides explícitos por `presentation`, `variant`, `icon`, `icons`, `tooltip` e `fallback`. O
26
+ core ganha `presentDictionaryValue` e `dictionaryDescriptor` (sem React) e o tipo
27
+ `DictEntryMeta` passa a morar em `@softize/opus/core`, reexportado pelo driver Zod. `Badge` ganha
28
+ a variante tonal `danger`.
29
+
30
+ `ActionList` reconhece colunas de dicionário sem configuração duplicada: campo de saída com a meta
31
+ de `t.dict` renderiza `DictionaryValue`, e `ListColumnSpec.dictionary` nomeia um dicionário do
32
+ provider para campos de string comum. `cells` continua vencendo. Dicionários sem `presentation`
33
+ continuam como texto; a única mudança sem opt-in é que uma coluna de dicionário sem célula custom
34
+ passa a mostrar o rótulo em vez do código, inclusive no chip de `type: 'badge'`.
35
+
36
+ Valor ausente ganha representação padrão. `EmptyValue` mostra o travessão com “Não informado” à
37
+ leitura assistiva em célula compacta e o rótulo em texto corrido; `label` dá o significado do
38
+ domínio. `isAbsentValue` define a regra: `null`, `undefined`, string vazia ou só espaços; `0`,
39
+ `false` e coleções vazias são valores. Colunas escalares de `ActionList` deixam de ficar em branco
40
+ e passam a usar essa representação, com `ListColumnSpec.empty` para o texto do domínio (array e
41
+ objeto sem `cells` seguem sem representação padrão); `DetailField` mostra “Não informado” ou
42
+ `empty`. Condicionais locais de `—` podem ser removidas.
43
+
44
+ `Pagination` deixa de ser byte-fiel ao shadcn: os números têm altura fixa e largura mínima
45
+ quadrada que cresce com os dígitos (`h-9 min-w-9 w-auto tabular-nums`), `page` dá o conteúdo e o
46
+ nome acessível “Página N”, sem `href` o link vira `<button>` com foco e `disabled`, e as setas
47
+ falam pt-BR (`label` localiza, `iconOnly` deixa só a seta quadrada, `iconClassName` dimensiona o
48
+ svg). `ActionList` compõe essa primitiva no rodapé e remove o paginador paralelo montado com
49
+ `Button`; 5726 e 5727 já não se sobrepõem. **Breaking:** o default de `size` em `PaginationLink`
50
+ passa de `icon` para `default`; quem dependia do quadrado fixo `size-9` num número declara
51
+ `size="icon"`. Quem usava `PaginationPrevious`/`PaginationNext` com o texto em inglês passa a ver
52
+ “Página anterior”/“Próxima página”, e a elipse deixa de carregar texto assistivo morto.
53
+
54
+ Selects inline de `ActionFilterBar` passam a ter largura fixa e previsível (`w-40`; lookup e
55
+ múltiplo `w-52`): a label trunca em vez de alargar o filtro e o valor selecionado trunca no
56
+ trigger, com a opção inteira na lista. O modal de filtros avançados continua com `w-full`.
57
+
58
+ As skills `model-opus-dictionary` e `build-opus-ui` passam a exigir a classificação do papel de
59
+ apresentação e trazem a tabela de decisão, exemplos e avaliações que reprovam badge indiscriminado,
60
+ dimensões independentes misturadas na mesma célula, status apresentado como classificação, tooltip
61
+ que repete o rótulo, ícone inventado, comunicação somente por cor ou ícone, fallback de ausência
62
+ repetido à mão, paginador paralelo, número de página preso a largura fixa e select inline
63
+ dimensionado pelo conteúdo.
64
+
65
+ ## 12.7.2 — 2026-09-01
66
+
67
+ `opus check` deixa de exigir modo `755` exato nos executáveis de `.githooks/`. O Git rastreia só o
68
+ bit de execução, então um checkout com umask `002` nasce com `775` e o gate acusava drift onde não
69
+ havia — um falso vermelho em toda worktree nova. O check agora aceita qualquer modo executável pelo
70
+ dono; o que o `opus setup` grava (criação e reparo) continua saindo com `755` canônico, mas um
71
+ executável existente com modo válido não é mais reescrito.
72
+
73
+ O registry inclui a skill `model-opus-dictionary`: modelagem de vocabulário fechado com `t.dict`,
74
+ schema derivado por `.zod()`, registro em `defineDomain({ dicts })` e a decisão dicionário ×
75
+ lookup × discriminante local. `implement-opus-change` e `build-opus-ui` passam a rotear para ela.
76
+
10
77
  ## 12.7.1 — 2026-09-01
11
78
 
12
79
  No tema escuro, o item ativo de `Tabs` volta a usar uma superfície opaca, destacando a seleção
@@ -102,10 +102,20 @@ function renderDict(name, dict) {
102
102
  const entry = values[key]
103
103
  lines.push(` ${formatKey(key)}: ${renderEntry(entry)},`)
104
104
  }
105
- lines.push('})')
105
+ const opts = renderDictOpts(dict)
106
+ lines.push(opts === null ? '})' : `}, ${opts})`)
106
107
  return lines
107
108
  }
108
109
 
110
+ /** Opções do dict (`doc`, `presentation`) — preservadas no stub pra que o front leia o
111
+ * mesmo papel de apresentação que o servidor declarou. */
112
+ function renderDictOpts(dict) {
113
+ const parts = []
114
+ if (typeof dict.doc === 'string') parts.push(`doc: ${JSON.stringify(dict.doc)}`)
115
+ if (typeof dict.presentation === 'string') parts.push(`presentation: ${JSON.stringify(dict.presentation)}`)
116
+ return parts.length === 0 ? null : `{ ${parts.join(', ')} }`
117
+ }
118
+
109
119
  /**
110
120
  * Emite a chave como identifier nu (`open`) quando for válido em TS,
111
121
  * senão como literal string (`'foo-bar'`).
@@ -250,8 +250,14 @@ function serializeDicts(dicts) {
250
250
  for (const k of keys) {
251
251
  values[k] = entries[k] ?? null
252
252
  }
253
- // `doc` = entendimento do vocabulário inteiro (o que esse dict representa).
254
- out[name] = { keys, values, doc: typeof params.doc === 'string' ? params.doc : null }
253
+ // `doc` = entendimento do vocabulário inteiro (o que esse dict representa);
254
+ // `presentation` = papel de apresentação declarado (ADR 0003) null quando ausente.
255
+ out[name] = {
256
+ keys,
257
+ values,
258
+ doc: typeof params.doc === 'string' ? params.doc : null,
259
+ presentation: typeof params.presentation === 'string' ? params.presentation : null,
260
+ }
255
261
  continue
256
262
  }
257
263
  // Fallback: dict-like com `keys()` + `metaFor()`.
@@ -355,7 +355,10 @@ function reconcileFile(root, destination, item, mode, errors, changes) {
355
355
  if (currentFile === null) return
356
356
  const executable = destination.startsWith('.githooks/')
357
357
  const requiredMode = executable ? 0o755 : undefined
358
- const modeIsCurrent = !executable || (currentFile.mode & 0o777) === requiredMode
358
+ // O Git rastreia o bit de execução; o modo efetivo vem do umask do checkout (755 com 022,
359
+ // 775 com 002). Exigir 755 exato reprovava worktrees legítimas — o check aceita qualquer modo
360
+ // executável pelo dono, e a correção continua gravando 755 canônico.
361
+ const modeIsCurrent = !executable || (currentFile.mode & 0o100) !== 0
359
362
  const contentIsCurrent =
360
363
  currentFile.exists &&
361
364
  (executable ? currentFile.content === item.output : managedTextEquivalent(currentFile.content, item.output))
@@ -601,7 +604,7 @@ function reconcileSharedPrePush(root, mode, errors, changes) {
601
604
  }
602
605
  return
603
606
  }
604
- if ((current.mode & 0o777) !== 0o755) {
607
+ if ((current.mode & 0o100) === 0) {
605
608
  if (mode === 'check') errors.push(`${destination}: modo executável incorreto.`)
606
609
  else if (replaceMaterializedFile(root, destination, expected, current, errors, { mode: 0o755 })) {
607
610
  changes.push(`corrigido modo executável de ${destination}`)
@@ -0,0 +1,160 @@
1
+ # ADR 0003 — A apresentação de um dicionário é declarada, não inferida
2
+
3
+ - Status: aceita
4
+ - Data: 2026-09-02
5
+
6
+ ## Contexto e forças
7
+
8
+ `t.dict` declara vocabulário fechado uma única vez: código estável, rótulo, documentação de
9
+ negócio e metadata livre por entrada. O manifest projeta esse vocabulário, a interface obtém
10
+ rótulos por `labelFor`/`metaFor` e os campos e filtros resolvem opções por
11
+ `options: { kind: 'dictionary', ref }` ou pela meta que viaja no schema do contrato.
12
+
13
+ O que o dicionário não declara é como o valor deve aparecer. Cada tela decide sozinha se o valor
14
+ vira texto, badge `outline`, badge tonal, ícone ou tooltip. Quando um agente gera a interface,
15
+ ele infere essa apresentação a partir do nome do dicionário e do gosto do momento. Na Grand
16
+ Brasil, “Pessoa física/Empresa” apareceu como texto secundário sob o nome enquanto
17
+ “Prospect/Cliente” apareceu como badge: duas dimensões independentes (tipo e estágio) foram lidas
18
+ como uma hierarquia, e a pessoa interpretou o tipo como explicação do estágio.
19
+
20
+ O problema não pertence a essa tela. Ele se repete em toda coleção com mais de um dicionário,
21
+ porque a única informação disponível para a interface é “este valor vem de um dicionário”, o que
22
+ não distingue classificação de status, nem status de estágio. As forças em tensão:
23
+
24
+ - o vocabulário precisa continuar declarado uma única vez, sem catálogo paralelo de cores e
25
+ ícones em cada tela;
26
+ - o core do Opus precisa continuar isomórfico e sem React, porque o contrato viaja para servidor,
27
+ manifest, Lens e agentes;
28
+ - dicionários existentes não podem mudar de aparência sem opt-in, e um dicionário sem metadata
29
+ precisa de um comportamento seguro;
30
+ - ícone e cor não podem ser a única forma de comunicar um valor;
31
+ - a orientação para agentes precisa ser verificável por avaliações, não apenas por prosa.
32
+
33
+ ## Alternativas consideradas
34
+
35
+ ### Manter a apresentação em cada tela
36
+
37
+ É o estado atual. Preserva flexibilidade total, mas mantém a inferência como regra: cada agente
38
+ decide de novo, e o mesmo dicionário aparece de formas diferentes em telas vizinhas. A observação
39
+ registrada na Grand Brasil é a segunda ocorrência do mesmo defeito em superfícies distintas.
40
+ Rejeitada.
41
+
42
+ ### Inferir o papel pelo nome ou pelas chaves
43
+
44
+ Nomes como `Status` ou chaves como `open`/`closed` sugerem um status, mas `kind`, `type`,
45
+ `category` e `stage` não se distinguem por convenção de nome, e a inferência falha justamente nos
46
+ casos ambíguos que causaram o problema. Também moveria uma decisão de domínio para uma heurística
47
+ do SDK. Rejeitada.
48
+
49
+ ### Tratar toda entrada de dicionário como badge
50
+
51
+ Resolve a inconsistência ao custo de ruído: tabelas densas ficam cheias de chips, e a diferença
52
+ entre classificação e estado desaparece. A convenção da casa reserva o badge tonal para estado
53
+ semântico. Rejeitada.
54
+
55
+ ### Declarar o papel no dicionário e concentrar os defaults em um renderer
56
+
57
+ O dicionário declara seu papel de apresentação; cada entrada pode declarar tom, ícone e uma
58
+ descrição curta; um renderer compartilhado aplica os defaults e aceita override explícito. O
59
+ manifest projeta a metadata e a skill de UI orienta o agente a partir dela. Aceita.
60
+
61
+ ## Decisão
62
+
63
+ ### Contrato
64
+
65
+ O core ganha o módulo `dictionary` com o vocabulário fechado de apresentação, independente de
66
+ React:
67
+
68
+ - `DictPresentation = 'classification' | 'status' | 'stage' | 'plain'`, declarado no nível do
69
+ dicionário em `t.dict(entries, { presentation })`;
70
+ - `DictTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger'`, declarado por entrada em
71
+ `tone`, explícito e restrito a status e estágios;
72
+ - `icon` por entrada continua uma string, agora definida como identificador estável do catálogo
73
+ de ícones do Opus (`iconPickerIcons`); nome fora do catálogo não renderiza ícone;
74
+ - `description` por entrada é o texto curto voltado à pessoa, usado pelo tooltip. `doc` continua
75
+ sendo o entendimento de negócio para manifest, Lens e agentes e não vira tooltip.
76
+
77
+ `t.dict` valida `presentation` e `tone` em tempo de construção e falha com mensagem clara,
78
+ inclusive quando `tone` aparece fora de status e estágio. A meta
79
+ lógica passa a carregar `params.presentation`, e o manifest projeta `presentation` ao lado de
80
+ `doc`. O tipo `DictEntryMeta` migra para o core e é reexportado pelo driver Zod; a API pública não
81
+ muda.
82
+
83
+ ### Defaults de apresentação
84
+
85
+ O core expõe `presentDictionaryValue(descriptor, value)`, a função pura que aplica a tabela de
86
+ decisão:
87
+
88
+ | Papel | Forma | Variante | Ícone | Tooltip |
89
+ | --- | --- | --- | --- | --- |
90
+ | `classification` | badge | `outline` | se declarado | se `description` acrescentar |
91
+ | `status` | badge | tonal por `tone`, `neutral` sem tom | se declarado | se `description` acrescentar |
92
+ | `stage` | badge | tonal por `tone`, `neutral` sem tom | se declarado | se `description` acrescentar |
93
+ | `plain` ou ausente | texto | — | se declarado | se `description` acrescentar |
94
+
95
+ Status e estágio nunca usam `outline`. Classificação não aceita `tone` (o construtor rejeita, e
96
+ metadata vinda de fora é ignorada), porque a diferença visual entre classificação e estado é o
97
+ que permite ler duas dimensões como independentes. Valor fora do dicionário é texto mesmo com
98
+ `variant` sobreposto. Tooltip só
99
+ existe quando `description` está presente e difere do rótulo. Valor fora do dicionário renderiza
100
+ o código como texto, sem badge, sem ícone e sem tooltip.
101
+
102
+ ### Renderer
103
+
104
+ `@softize/opus/ui/react` exporta `DictionaryValue`, que recebe o dicionário e o valor e aplica os
105
+ defaults acima. Overrides são explícitos por props: `presentation`, `variant`, `icon`, `icons`,
106
+ `tooltip` e `fallback`. O rótulo é sempre renderizado como texto; o ícone é decorativo
107
+ (`aria-hidden`) e o badge com tooltip é focável para que a descrição alcance o teclado. O
108
+ `Badge` ganha a variante tonal `danger`, completando a família `success`/`warning`/`info` sem
109
+ reutilizar o `destructive` sólido.
110
+
111
+ ### Colunas declarativas
112
+
113
+ `ActionList` passa a reconhecer que uma coluna do contrato mostra um dicionário por dois
114
+ caminhos, sem configuração duplicada:
115
+
116
+ 1. zero-config: a coluna cujo campo no schema de saída carrega a meta de `t.dict` renderiza
117
+ `DictionaryValue` a partir dessa meta;
118
+ 2. explícito: `ListColumnSpec.dictionary` nomeia a referência registrada em
119
+ `TbdlibProvider dicts`, para colunas cujo schema de saída é uma string comum.
120
+
121
+ `cells` continua vencendo qualquer coluna derivada. A única mudança de comportamento sem opt-in é
122
+ que uma coluna de dicionário sem célula custom passa a mostrar o rótulo em vez do código; a
123
+ apresentação visual (badge, tom, ícone, tooltip) exige `presentation` declarado. `type: 'badge'`
124
+ continua funcionando como chip `outline`, agora com o rótulo quando a coluna é dicionário.
125
+
126
+ ### Orientação
127
+
128
+ A skill `model-opus-dictionary` passa a exigir a classificação do papel de apresentação junto com
129
+ a decisão dicionário × lookup × discriminante. A skill `build-opus-ui` ganha a tabela de decisão,
130
+ exemplos positivos e negativos e avaliações que reprovam badge indiscriminado, dimensões
131
+ misturadas na mesma célula, status apresentado como classificação, classificação tratada como
132
+ metadado irrelevante, tooltip que repete o rótulo, ícone inventado, comunicação somente por cor ou
133
+ ícone e excesso de badges em tabelas densas.
134
+
135
+ ## Consequências
136
+
137
+ Dicionários existentes continuam renderizando como hoje até declararem `presentation`. Um
138
+ dicionário com `color` livre não recebe tom automático: `color` permanece metadata livre, lida
139
+ pela Lens, e o renderer honra apenas `tone`. Projetos que já usam `description` como texto de
140
+ apoio em opções passam a ter o mesmo campo alimentando o tooltip, o que é o uso pretendido.
141
+
142
+ O catálogo de ícones do Opus torna-se a fronteira do que um agente pode declarar. Vocabulário
143
+ fora dele exige extensão do catálogo ou `icons` no renderer, nunca um nome inventado.
144
+
145
+ A Lens ainda lê apenas `label` e `color` das entradas; projetar `presentation`, `tone` e `icon`
146
+ no inspetor é evolução separada. Selects de formulário continuam mostrando somente rótulos.
147
+
148
+ ## Verificação
149
+
150
+ - testes do core para a tabela de decisão nos quatro papéis, tom, ícone, descrição igual ao
151
+ rótulo e valor desconhecido;
152
+ - testes de `t.dict` para validação de `presentation` e `tone` e para a meta projetada;
153
+ - testes de React para `DictionaryValue` cobrindo os quatro papéis, overrides, texto sempre
154
+ presente, ícone decorativo e tooltip focável;
155
+ - testes de `ActionList` para coluna zero-config, coluna com `dictionary` explícito, precedência
156
+ de `cells` e compatibilidade de `type: 'badge'`;
157
+ - teste de manifest provando `presentation` projetado e entradas com `tone`, `icon` e
158
+ `description`;
159
+ - validação das skills com casos positivo, negativo e de execução;
160
+ - typecheck, suíte do pacote, `opus check`, `opus copy --check` e gates de materialização.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "12.7.1",
3
+ "version": "12.8.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,7 @@
2
2
  name: build-opus-ui
3
3
  description: Constrói interface contract-driven com hooks, forms, listas, views e componentes de @softize/opus/ui. Use ao implementar ou alterar telas que consomem actions Opus.
4
4
  ---
5
+ <!-- softize-skill-route: $model-opus-dictionary -->
5
6
 
6
7
  # Construir UI Opus
7
8
 
@@ -14,33 +15,50 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
14
15
 
15
16
  1. Identificar o contrato e o kind da action; corrigir lacuna do contrato na fonte em vez
16
17
  de compensá-la com tipo ou validação na UI.
17
- 2. Usar os hooks e componentes do catálogo `@softize/opus/ui/react` adequados ao kind.
18
- 3. Tratar página, filtro, seleção e modal importante como estado navegável por URL quando
18
+ 2. Usar os hooks e componentes do catálogo `@softize/opus/ui/react` adequados ao kind. Rótulo e
19
+ mensagem de vocabulário fechado vêm do dicionário do domínio (`labelFor`/`metaFor`); ao
20
+ encontrar catálogo repetido à mão ou vocabulário ainda sem dicionário, carregar
21
+ `$model-opus-dictionary`.
22
+ 3. Apresentar valor de dicionário por `DictionaryValue` ou pela coluna de `ActionList`, que
23
+ aplicam o papel declarado em `presentation` (classificação em badge `outline`, status e estágio
24
+ em badge tonal, `plain` em texto). Não escolher badge, tom ou ícone pelo nome do dicionário:
25
+ dicionário sem papel declarado renderiza texto e pede classificação por
26
+ `$model-opus-dictionary` antes de qualquer destaque visual. Sobrepor os defaults só com motivo
27
+ explícito nas props do renderer.
28
+ 4. Dar a cada dimensão independente usada para comparação ou filtro um campo, coluna ou espaço
29
+ identificável próprio, com rótulo. Hierarquia tipográfica (texto secundário sob um nome) não
30
+ pode fazer uma dimensão parecer explicação de outra. Cor e ícone reforçam; o texto do valor
31
+ permanece sempre presente.
32
+ 5. Deixar ausência, paginação e largura de filtro com o pattern: célula e `DetailField` já
33
+ representam valor ausente (`EmptyValue`, `empty` para o significado do domínio); listas
34
+ paginam pela primitiva `Pagination`; selects inline de filtro têm largura fixa. Não reescrever
35
+ esses defaults na tela.
36
+ 6. Tratar página, filtro, seleção e modal importante como estado navegável por URL quando
19
37
  o produto precisa de deep link, back/forward ou refresh.
20
- 4. Compor páginas com `Page` e seu teto centralizado padrão de `72rem`. Usar
38
+ 7. Compor páginas com `Page` e seu teto centralizado padrão de `72rem`. Usar
21
39
  `ContentHeader` em seções que precisam da mesma estrutura de título, descrição,
22
40
  metadados e ações; ajustar `level` pela hierarquia semântica, não pelo destaque visual.
23
- 5. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
41
+ 8. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
24
42
  seu interior.
25
- 6. Não definir a fonte raiz em uma biblioteca ou componente. O navegador e a aplicação são
43
+ 9. Não definir a fonte raiz em uma biblioteca ou componente. O navegador e a aplicação são
26
44
  responsáveis por `font-size` em `html`; medidas escaláveis da UI usam `rem` ou a escala
27
45
  relativa do Tailwind. Reservar `px` a hairlines e compensações presas à geometria da borda,
28
46
  com justificativa e cobertura explícitas.
29
- 7. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
30
- 8. Tratar tokens de superfície como pares indivisíveis no mesmo fragmento de classes:
47
+ 10. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
48
+ 11. Tratar tokens de superfície como pares indivisíveis no mesmo fragmento de classes:
31
49
  `bg-card text-card-foreground` e `bg-popover text-popover-foreground`. Não depender da
32
50
  igualdade atual com `--foreground`, porque o app pode sobrescrever cada par.
33
- 9. Usar diretamente a escala `rounded-*`; não criar radius por nome de componente quando
51
+ 12. Usar diretamente a escala `rounded-*`; não criar radius por nome de componente quando
34
52
  `rounded-xs` a `rounded-2xl` já expressam a forma. Escolher o degrau pela escala visual:
35
53
  detalhes e itens internos tendem a `xs`/`sm`, controles e flutuantes compactos a `md`,
36
54
  molduras estruturais a `lg`, e superfícies principais ou modais a `xl`. Essa associação
37
55
  orienta o default, não cria uma restrição semântica. Em aninhamento, evitar moldura dupla e
38
56
  reduzir o raio interno; em grupos conectados, remover os raios das arestas internas. Tamanho
39
57
  e forma permanecem eixos separados; usar `shape="pill"` quando a pílula for intencional.
40
- 10. Distinguir uma região disponível para criação ou vínculo, representada por `Empty` com
58
+ 13. Distinguir uma região disponível para criação ou vínculo, representada por `Empty` com
41
59
  moldura tracejada, de um resultado vazio dentro de uma estrutura existente, que preserva
42
60
  a moldura sólida dessa estrutura.
43
- 11. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
61
+ 14. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
44
62
 
45
63
  ## Verificação
46
64
 
@@ -58,6 +76,9 @@ Inspecionar visualmente a rota real e validar navegação por URL quando aplicá
58
76
  - Não criar fetch, schema ou tipo paralelo ao contrato.
59
77
  - Não copiar componente da lib para customizar sem antes verificar extensão/composição.
60
78
  - Não forçar modal roteável quando o estado é efêmero e sem valor de navegação.
79
+ - Não inferir apresentação de dicionário: nem badge para todo valor, nem tom ou ícone
80
+ inventados, nem tooltip que repete o rótulo.
81
+ - Não repetir fallback de ausência, paginador ou largura de filtro que o pattern já resolve.
61
82
 
62
83
  ## Recursos
63
84
 
@@ -17,3 +17,28 @@
17
17
  - Reprova: reconstruir manualmente o container de página, usar `Empty` tracejado como vazio de
18
18
  tabela, alinhar toda coluna numérica à direita por inferência ou destacar “Cancelar” como ação
19
19
  primária em modal.
20
+ - Execução de dicionários: montar a lista de clientes com “Pessoa física/Empresa” e
21
+ “Prospect/Cliente”; provar que o tipo ocupa a coluna “Tipo” como classificação (badge `outline`
22
+ com os ícones declarados `user` e `building`), que o estágio ocupa a coluna “Estágio” como badge
23
+ tonal sem `outline`, que nenhuma das duas usa `cells`, que o texto do valor está presente e que
24
+ não há tooltip em “Pessoa física/Empresa” quando a descrição não acrescenta ao rótulo.
25
+ - Reprova: envolver todo valor de dicionário em badge sem papel declarado, ou escolher a variante
26
+ pelo nome do dicionário.
27
+ - Reprova: colocar duas dimensões independentes na mesma célula sem identificação, como o tipo em
28
+ texto secundário sob o nome enquanto o estágio ganha badge.
29
+ - Reprova: apresentar status ou estágio com `outline`, ou tratar uma classificação como metadado
30
+ irrelevante em texto muted quando ela é dimensão de comparação ou filtro.
31
+ - Reprova: tooltip cuja descrição apenas repete o rótulo, ou `doc` de negócio exibido como tooltip.
32
+ - Reprova: ícone inferido ou inventado pela IA, fora do catálogo `iconPickerIcons` ou não declarado
33
+ na entrada do dicionário.
34
+ - Reprova: comunicar um valor somente por cor ou ícone (`Dot` sem texto, badge vazio).
35
+ - Reprova: tabela densa com badge em todas as colunas de dicionário quando só status e estágio
36
+ pedem destaque.
37
+ - Reprova: repetir à mão o fallback de valor ausente (`item.email === '' ? '—' : item.email`) em
38
+ células ou campos quando a coluna, o `DetailField` ou `EmptyValue` já representam a ausência.
39
+ - Reprova: montar um paginador paralelo com `Button` dentro de `ActionList` ou de outra lista em
40
+ vez de compor `Pagination`.
41
+ - Reprova: prender o número de página a largura fixa (`size-7 p-0`), fazendo 5726 e 5727 se
42
+ sobreporem.
43
+ - Reprova: dimensionar select inline de filtro pelo conteúdo ou pela label (`min-w-*` solto,
44
+ largura calculada), em vez da largura fixa do pattern.
@@ -42,4 +42,89 @@
42
42
  Quando uma tabela, card ou grupo existente apenas não tem resultados, manter sua moldura
43
43
  sólida e renderizar o vazio estrutural dentro dela; não trocar toda ausência de dados por
44
44
  `Empty`.
45
+ - Valor de dicionário é apresentado por `DictionaryValue` ou pela coluna de `ActionList`; o
46
+ papel vem de `presentation` no `t.dict`, nunca do nome do dicionário nem do gosto da tela.
47
+ - Valor ausente (`null`, `undefined`, vazio, só espaços) tem representação padrão: coluna de
48
+ `ActionList` e `DetailField` já mostram travessão ou “Não informado”; o significado do domínio
49
+ entra por `ListColumnSpec.empty` ou `DetailField empty`, e renderer customizado usa
50
+ `EmptyValue`. `0` e `false` são valores. Não repetir a condicional de `—` em cada célula.
51
+ - Paginação é a primitiva `Pagination`, inclusive dentro de `ActionList`. Número de página tem
52
+ largura mínima e cresce com os dígitos; só as setas são quadradas. Não montar paginador com
53
+ `Button` nem prender números a `size-*`.
54
+ - Select inline de filtro tem largura fixa (`w-40`; lookup e múltiplo `w-52`): a label trunca, o
55
+ valor trunca no trigger e a opção inteira fica na lista. Não dimensionar filtro pelo conteúdo
56
+ nem pela label; o modal usa `w-full`.
45
57
  - Catálogo e API efetivos vêm dos exports da versão instalada, não de memória ou exemplo antigo.
58
+
59
+ ## Dicionários e dimensões
60
+
61
+ O dicionário declara o papel; a tela só escolhe onde o valor fica. Tabela de decisão aplicada
62
+ por `DictionaryValue` e pelas colunas de `ActionList`:
63
+
64
+ | Papel declarado | Exemplos | Forma | Variante | Ícone | Tooltip |
65
+ |---|---|---|---|---|---|
66
+ | `classification` | Tipo de cliente, categoria, natureza | Badge | `outline`; ignora `tone` | se a entrada declara `icon` do catálogo | se `description` acrescenta ao rótulo |
67
+ | `status` | Aberto, resolvido, degradado | Badge | tonal pelo `tone`; `neutral` sem tom; nunca `outline` | idem | idem |
68
+ | `stage` | Prospect, cliente; etapa do funil | Badge | igual a `status` | idem | idem |
69
+ | `plain` ou ausente | Fonte, formato, período | Texto | — | idem | idem |
70
+
71
+ Regras que não dependem da tabela:
72
+
73
+ - Dimensão independente usada para comparação ou filtro ocupa coluna, campo ou espaço
74
+ identificável próprio, com rótulo. Duas dimensões na mesma célula sem identificação viram
75
+ hierarquia falsa.
76
+ - O texto do valor está sempre presente; cor e ícone reforçam, não substituem.
77
+ - Tabela densa com várias colunas de dicionário mantém badge só nas dimensões de status e estágio;
78
+ classificações secundárias podem ficar em texto (`presentation="plain"` na ocorrência) quando o
79
+ excesso de chips prejudica a leitura.
80
+ - Sem `presentation` declarado, o valor é texto. A correção é classificar o dicionário
81
+ (`$model-opus-dictionary`), não escolher uma variante na tela.
82
+
83
+ Exemplo correto — tipo e estágio em colunas próprias, apresentação do dicionário:
84
+
85
+ ```ts
86
+ export const customerKindDict = t.dict(
87
+ { pf: { label: 'Pessoa física', icon: 'user' }, pj: { label: 'Empresa', icon: 'building' } },
88
+ { doc: 'Natureza da parte no cadastro global.', presentation: 'classification' },
89
+ )
90
+ export const customerStageDict = t.dict(
91
+ { prospect: { label: 'Prospect' }, customer: { label: 'Cliente', tone: 'success' } },
92
+ { doc: 'Estágio comercial atual da parte.', presentation: 'stage' },
93
+ )
94
+ // contrato:
95
+ // columns: [{ key: 'name' }, { key: 'kind', label: 'Tipo' }, { key: 'stage', label: 'Estágio' }]
96
+ // output: z.object({ kind: customerKindDict.zod(), stage: customerStageDict.zod() })
97
+ // tela: <ActionList action={customerListContract} input={{}} /> — sem cells para kind e stage
98
+ ```
99
+
100
+ Exemplos incorretos:
101
+
102
+ ```tsx
103
+ // Dimensões misturadas: o tipo vira "explicação" do nome; o estágio vira badge à mão.
104
+ cells={{
105
+ name: (item) => (
106
+ <div>
107
+ <p className="font-medium">{item.name}</p>
108
+ <span className="text-xs text-muted-foreground">
109
+ {item.kind === 'pj' ? 'Empresa' : 'Pessoa física'}
110
+ </span>
111
+ </div>
112
+ ),
113
+ stage: (item) =>
114
+ item.stage === 'customer'
115
+ ? <Badge variant="secondary">Cliente</Badge>
116
+ : <Badge variant="outline">Prospect</Badge>,
117
+ }}
118
+
119
+ // Badge para tudo: fonte, formato e período viram chips sem papel declarado.
120
+ <Badge variant="outline">{reportFormatDict.labelFor(item.format)}</Badge>
121
+
122
+ // Status apresentado como classificação (outline) e ícone inventado fora do catálogo.
123
+ <Badge variant="outline"><Sparkles /> {statusDict.labelFor(item.status)}</Badge>
124
+
125
+ // Tooltip que repete o rótulo; cor como único sinal.
126
+ <Tooltip>
127
+ <TooltipTrigger><Dot variant="success" /></TooltipTrigger>
128
+ <TooltipContent>Cliente</TooltipContent>
129
+ </Tooltip>
130
+ ```
@@ -6,6 +6,7 @@ description: Implementa uma mudança em projeto baseado no Opus preservando cont
6
6
  <!-- softize-skill-route: $test-opus-action -->
7
7
  <!-- softize-skill-route: $build-opus-ui -->
8
8
  <!-- softize-skill-route: $create-opus-seed -->
9
+ <!-- softize-skill-route: $model-opus-dictionary -->
9
10
 
10
11
  # Implementar mudança Opus
11
12
 
@@ -26,9 +27,9 @@ dependências server-only, bindings registrados, testes e gates verdes.
26
27
  2. Modelar primeiro o contrato observável: nome, descrição, input, output, erros e metadata.
27
28
  3. Manter código compartilhável fora de banco, segredo, filesystem e drivers server-only.
28
29
  4. Implementar o binding e registrar o `ActionDef` no domínio/runtime conforme a topologia local.
29
- 5. Carregar e seguir `$create-opus-action`, `$test-opus-action`, `$build-opus-ui` ou
30
- `$create-opus-seed` antes
31
- do passo correspondente quando a mudança entrar nesses workflows especializados.
30
+ 5. Carregar e seguir `$create-opus-action`, `$test-opus-action`, `$build-opus-ui`,
31
+ `$create-opus-seed` ou `$model-opus-dictionary` antes do passo correspondente quando a
32
+ mudança entrar nesses workflows especializados.
32
33
  6. Atualizar manifest, docs geradas e exemplos somente pelos comandos do repo.
33
34
 
34
35
  ## Verificação
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: model-opus-dictionary
3
+ description: Modela vocabulário fechado de domínio com t.dict, schema derivado e registro no domínio, decidindo entre dicionário, lookup e discriminante local. Use ao introduzir ou migrar códigos estáveis que contrato, regra, interface ou auditoria reutilizam.
4
+ ---
5
+
6
+ # Modelar dicionário Opus
7
+
8
+ ## Resultado
9
+
10
+ Entregar o vocabulário declarado uma única vez: códigos estáveis com rótulo e documentação em
11
+ `t.dict`, schema derivado por `.zod()`, registro em `defineDomain({ dicts })` e consumidores lendo
12
+ da mesma instância, sem catálogo repetido em enum, opção estática ou formatador local.
13
+
14
+ ## Entradas
15
+
16
+ - códigos, rótulos e significado de negócio do vocabulário, com quem os reutiliza;
17
+ - contrato, regra, interface ou auditoria que hoje repetem o catálogo;
18
+ - gates do projeto para representações legadas (baseline de `z.enum`/opções estáticas), quando
19
+ existirem.
20
+
21
+ ## Procedimento
22
+
23
+ 1. Classificar o vocabulário antes de codificar:
24
+ - **dicionário** — fechado nesta versão, códigos estáveis, significado de domínio reutilizado
25
+ por mais de um consumidor;
26
+ - **lookup** — administrável, pesquisável, autorizável ou vindo do banco: expor por action
27
+ `kind: 'list'`/lookup, nunca por opção estática;
28
+ - **discriminante local** — enum ou literal privado de transporte/controle que não repete um
29
+ catálogo apresentado nem uma regra compartilhada: permanece local e fora do dicionário.
30
+ 2. Declarar o dicionário com `t.dict`, no módulo de dicionários do domínio: chave é código de
31
+ máquina persistível; rótulo, documentação e demais metadata evoluem sem migração de dados.
32
+ 3. Classificar o papel de apresentação e declará-lo em `presentation`, porque a interface não o
33
+ infere:
34
+ - **classification** — tipo, categoria ou natureza: dimensão estável de comparação;
35
+ - **status** — situação operacional que muda com o tempo;
36
+ - **stage** — etapa de um ciclo ou funil;
37
+ - **plain** — valor que só precisa ser legível (o mesmo efeito de omitir).
38
+ Por entrada, declarar `tone` somente em status e estágio, `icon` somente com nome existente no
39
+ catálogo `iconPickerIcons`, e `description` somente quando acrescentar algo ao rótulo. `doc`
40
+ continua sendo o entendimento de negócio para manifest e Lens; não é tooltip.
41
+ 4. Derivar o schema por `.zod()` e usá-lo em todo contrato que valide o código; não redeclarar a
42
+ união em `z.enum` nem em tipo literal paralelo.
43
+ 5. Registrar o dicionário em `defineDomain({ dicts })` para que manifest e Lens o projetem.
44
+ 6. Migrar os consumidores para a mesma instância: `DictionaryValue` ou a coluna de `ActionList`
45
+ para apresentar o valor, `labelFor`/`metaFor` nas mensagens; remover o catálogo repetido de
46
+ formatadores, selects, mapas de variante de badge e comparações.
47
+ 7. Manter autorização e invariantes no servidor: metadata de dicionário orienta a interface, mas
48
+ não é barreira de integridade. Quando auditoria precisar preservar o que a pessoa viu,
49
+ armazenar o código e um snapshot do texto no momento do evento.
50
+ 8. Ao reduzir uma representação legada coberta por baseline de gate, apertar o baseline no mesmo
51
+ diff. Ao introduzir uma nova ocorrência fora do dicionário, classificá-la e justificá-la no
52
+ próprio diff; atualizar baseline não é correção automática.
53
+
54
+ ## Verificação
55
+
56
+ Rodar `opus check`, typecheck e os testes afetados; conferir no manifest que o domínio projeta o
57
+ dicionário com `presentation`; provar que o schema rejeita código fora do catálogo e que a
58
+ interface obtém rótulo e apresentação da mesma instância. Se a mudança alterou texto humano de
59
+ contrato ou componente mapeado, regenerar com `opus copy` e executar os checks de copy do projeto.
60
+
61
+ ## Limites
62
+
63
+ - Não migrar todos os enums de uma vez; a adoção é incremental e cada ocorrência exige
64
+ classificação própria.
65
+ - Não transformar discriminante local legítimo em dicionário por semelhança superficial.
66
+ - Não usar tabela ou action de lookup para valores compilados e estáveis; nem dicionário para
67
+ dados administráveis de runtime.
68
+ - Não tratar metadata de dicionário como autorização ou invariante de integridade.
69
+ - Não omitir `presentation` esperando que a tela deduza o papel pelo nome do dicionário; sem
70
+ papel declarado, o valor é texto.
71
+ - Não declarar `tone` em classificação, nem `icon` fora do catálogo, nem `description` que
72
+ repita o rótulo.
73
+
74
+ ## Recursos
75
+
76
+ - Use [avaliações](references/evaluations.md) ao evoluir esta skill.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Modelar dicionário Opus"
3
+ short_description: "Declara vocabulário fechado uma única vez com t.dict"
4
+ default_prompt: "Use $model-opus-dictionary para introduzir ou migrar este vocabulário fechado."
@@ -0,0 +1,18 @@
1
+ # Avaliações
2
+
3
+ - Positiva: “Os motivos de conflito de clientes aparecem repetidos no contrato, na UI e nas
4
+ mensagens; centralize esse vocabulário.” Deve disparar, declarar `t.dict`, derivar `.zod()`,
5
+ registrar em `defineDomain({ dicts })` e migrar os consumidores para a mesma instância.
6
+ - Negativa: “Adicione um campo de status interno `pending | done` ao envelope de transporte desta
7
+ fila.” Não deve disparar quando o par é discriminante privado que não repete catálogo
8
+ apresentado nem regra compartilhada.
9
+ - Execução: migrar um `z.enum` legado coberto por baseline, apertar o baseline no mesmo diff e
10
+ provar que manifest projeta o dicionário, que o schema rejeita código fora do catálogo e que a
11
+ interface lê rótulos por `labelFor`/`metaFor`.
12
+ - Execução de apresentação: dado um cadastro com “Pessoa física/Empresa” e “Prospect/Cliente”,
13
+ declarar o primeiro como `classification` com ícones `user` e `building` do catálogo e o segundo
14
+ como `stage` com `tone` explícito, sem `description` que repita o rótulo; provar que o manifest
15
+ projeta `presentation` e que `t.dict` rejeita `presentation` ou `tone` fora do vocabulário.
16
+ - Reprova: declarar `tone` em uma classificação, inventar `icon` fora do catálogo, copiar `doc`
17
+ para `description` ou deixar `presentation` ausente em um status esperando que a tela infira o
18
+ badge.