@softize/opus 19.0.0 → 20.1.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 +239 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +8 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +109 -18
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/list.tsx +91 -92
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/presentation.tsx +52 -37
- package/src/ui/components/patterns/split.tsx +44 -4
- package/src/ui/components/patterns/surface-anatomy.ts +35 -8
- package/src/ui/components/patterns/surface-header.tsx +58 -25
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -1
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/dialog.tsx +42 -11
- package/src/ui/components/primitives/drawer.tsx +10 -2
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +141 -58
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +41 -15
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +119 -27
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +107 -24
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +38 -15
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +12 -4
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +118 -12
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +55 -5
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +3 -2
- package/src/ui/docs/content/skeleton.md +6 -1
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +19 -0
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +32 -32
- package/src/ui/react.tsx +1 -0
- package/src/vite/design.ts +16 -4
|
@@ -30,6 +30,13 @@ uma `section`. A escolha de `fieldset` não define o acabamento visual: quando `
|
|
|
30
30
|
presente, a moldura é contínua por padrão. Os filtros avançados preservam a legenda integrada à
|
|
31
31
|
borda declarando `frameVariant="legend"`.
|
|
32
32
|
|
|
33
|
+
### Emenda de 2026-09-26 — o acabamento `legend` também vale para `section`
|
|
34
|
+
|
|
35
|
+
A ADR 0025 estende `frameVariant="legend"` a `Content as="section"`, para que regiões de leitura
|
|
36
|
+
integrem o header à moldura sem declarar semântica de formulário, e permite `ContentMedia` na
|
|
37
|
+
legenda de um `fieldset`. A regra desta ADR se mantém: `as` escolhe o elemento e a semântica, e
|
|
38
|
+
`frameVariant` escolhe só o acabamento. Na legenda, contador, descrição e ações continuam inválidos.
|
|
39
|
+
|
|
33
40
|
## Consequências
|
|
34
41
|
|
|
35
42
|
- A mesma API nomeia e delimita regiões de leitura e grupos de formulário.
|
|
@@ -143,7 +143,7 @@ anteriores já os delimitaram:
|
|
|
143
143
|
|
|
144
144
|
| Slot | Card | Content | Page | Dialog | Drawer | Pane |
|
|
145
145
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
146
|
-
| Media | sim |
|
|
146
|
+
| Media | sim | sim (`ContentMedia`) | não | sim (`DialogMedia`) | sim | sim |
|
|
147
147
|
| Title | sim | sim | sim | sim | sim | sim |
|
|
148
148
|
| Description | sim | sim | não (ADR 0014) | não (ADR 0018) | não (ADR 0018) | não |
|
|
149
149
|
| Meta | não | sim (contador) | não (ADR 0009) | não | não | não |
|
|
@@ -163,6 +163,19 @@ de um cabeçalho remontam a linha com `span`, incluindo o ícone dentro do texto
|
|
|
163
163
|
`Content` permanece sem footer: é uma região nomeada dentro de outra superfície, e um rodapé
|
|
164
164
|
pertence à superfície que a hospeda.
|
|
165
165
|
|
|
166
|
+
#### Emenda de 2026-09-25 — Content também expõe mídia
|
|
167
|
+
|
|
168
|
+
A aplicação posterior da anatomia à tela do Evento mostrou que regiões de leitura podem precisar
|
|
169
|
+
da mesma identificação visual que um Card sem deixarem de ser `Content`. Sem um slot próprio, a
|
|
170
|
+
documentação e os consumidores voltavam a inserir o ícone dentro de `ContentTitle`, repetindo o
|
|
171
|
+
desvio que motivou esta ADR.
|
|
172
|
+
|
|
173
|
+
`ContentHeader` passa a aceitar `ContentMedia`, e o shorthand expõe `media`. A mídia usa a mesma
|
|
174
|
+
moldura de `IconTile` das demais famílias e integra o grupo visual do título. Esse contrato vale
|
|
175
|
+
tanto para `section` quanto para `fieldset`: `as` troca o elemento e a semântica, não a apresentação
|
|
176
|
+
de mídia e título. Na `legend`, contador, descrição e ações continuam indisponíveis porque não
|
|
177
|
+
nomeiam o grupo de campos.
|
|
178
|
+
|
|
166
179
|
### 7. Título: hierarquia e tamanho são independentes
|
|
167
180
|
|
|
168
181
|
O nível semântico vem de `level` (`1`–`6`) ou de `asChild`, e nunca do tamanho visual. O tamanho vem
|
|
@@ -231,6 +244,11 @@ seccionado com a primeira linha apagada.
|
|
|
231
244
|
Pela mesma razão, a regra que suprime o divisor entre um header e um rodapé adjacentes precisa
|
|
232
245
|
vencer a que o cria — não basta vir depois dela na folha, porque a ordem das variantes não é nossa.
|
|
233
246
|
|
|
247
|
+
**O recuo inferior da última região no layout contínuo segue a mesma lógica** (20.0.0). Ele vem de
|
|
248
|
+
`[&:last-child]:pb-*`, e um `pb-0` do consumidor não o desliga quando a região é a última da
|
|
249
|
+
superfície: para mudar esse respiro, a superfície troca de layout. Quem precisar mesmo sobrescrever
|
|
250
|
+
escreve a mesma variante (`[&:last-child]:pb-*`), que o merge reconhece como substituta.
|
|
251
|
+
|
|
234
252
|
## Consequências
|
|
235
253
|
|
|
236
254
|
- Uma mudança de espaçamento, divisor ou escala passa a ter um lugar só; hoje tem seis.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# ADR 0024 — A ocupação de altura se propaga por contexto
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-25.
|
|
5
|
+
- **Complementa:** ADRs 0017 e 0023.
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
`Page layout="fill"` limita a página à altura do shell, e `ActionList layout="fill"` ocupa a altura
|
|
10
|
+
disponível e rola somente os resultados. As duas metades já existiam desde a 18.3.0, mas não se
|
|
11
|
+
encontravam sozinhas: a altura só chega à lista se cada elemento entre a página e ela for uma coluna
|
|
12
|
+
flexível com `min-h-0`. `PageBody`, `Content` e `ContentBody` não faziam isso.
|
|
13
|
+
|
|
14
|
+
O efeito era mensurável no código e na documentação:
|
|
15
|
+
|
|
16
|
+
- a página da `ActionList` ensinava a escrever `flex min-h-0 flex-1 flex-col` em `PageBody`,
|
|
17
|
+
`Content` e `ContentBody` para o `fill` funcionar;
|
|
18
|
+
- a `Presentation` de lista em `layout="fill"` envolvia o corpo em `Content` e `ContentBody` sem essa
|
|
19
|
+
cadeia e não repassava `layout` à lista — o `fill` declarado nunca chegava a ela;
|
|
20
|
+
- quando uma classe faltava, a lista crescia com o conteúdo e o `fill` se comportava como `flow`,
|
|
21
|
+
sem erro nem aviso.
|
|
22
|
+
|
|
23
|
+
As forças em tensão:
|
|
24
|
+
|
|
25
|
+
- **Automação × previsibilidade.** A cadeia é mecânica e o consumidor erra ao montá-la à mão; mas
|
|
26
|
+
uma página `fill` também hospeda seções irmãs, cards e diálogos que não devem virar colunas
|
|
27
|
+
flexíveis por acidente.
|
|
28
|
+
- **Contexto React × DOM.** O contexto atravessa qualquer componente, inclusive portais e
|
|
29
|
+
contêineres que não repassam altura; a altura só atravessa o DOM elemento a elemento.
|
|
30
|
+
- **Default × explícito.** Uma lista dentro da cadeia quase sempre quer `fill`; uma lista em uma
|
|
31
|
+
seção secundária da mesma página quer `flow`.
|
|
32
|
+
|
|
33
|
+
## Decisão
|
|
34
|
+
|
|
35
|
+
### 1. A raiz publica, os elos republicam, a folha se registra
|
|
36
|
+
|
|
37
|
+
- **Raiz:** `Page layout="fill"` publica a cadeia. `Page layout="flow"` a interrompe. A
|
|
38
|
+
`Presentation` em página herda o comportamento da `Page` que monta.
|
|
39
|
+
- **Elos canônicos:** `PageBody`, `Content`, `ContentBody` e o corpo da `Presentation` leem a
|
|
40
|
+
cadeia e a republicam para os filhos. Na cadeia, carregam `data-fill="false"`.
|
|
41
|
+
- **Folha:** a `ActionList`. Sem `layout`, ela assume `fill` quando o elemento pai no DOM é um elo
|
|
42
|
+
(`data-fill`) e se registra na cadeia. Com `layout="fill"` explícito, registra-se mesmo sob um
|
|
43
|
+
contêiner próprio; com `layout="flow"` explícito, nunca se registra.
|
|
44
|
+
|
|
45
|
+
### 2. Só o caminho até a folha vira coluna flexível
|
|
46
|
+
|
|
47
|
+
Um elo aplica `flex min-h-0 flex-1 flex-col` e passa a `data-fill="true"` somente quando uma folha
|
|
48
|
+
se registrou abaixo dele. Seções irmãs sem lista mantêm a altura do próprio conteúdo, e uma página
|
|
49
|
+
`fill` sem lista renderiza exatamente como antes. O registro acontece em `useLayoutEffect`, antes
|
|
50
|
+
da primeira pintura.
|
|
51
|
+
|
|
52
|
+
### 3. Superfícies com anatomia própria interrompem a cadeia
|
|
53
|
+
|
|
54
|
+
`Card`, `Dialog`, `Drawer` e `Pane` interrompem a propagação no provider de layout que já
|
|
55
|
+
compartilham (ADR 0023). A altura do corpo delas é decisão delas, inclusive quando um portal as
|
|
56
|
+
renderiza dentro de uma página `fill`. A própria `ActionList` também interrompe a cadeia para o que
|
|
57
|
+
ela hospeda. Em `Presentation`, o `fill` continua valendo apenas para a superfície `page`.
|
|
58
|
+
|
|
59
|
+
### 4. O mecanismo é interno
|
|
60
|
+
|
|
61
|
+
O contexto e os hooks vivem em um módulo interno, fora da API pública. Um contêiner próprio entre o
|
|
62
|
+
elo e a lista não é detectado como elo: quem o escreve declara `layout="fill"` na lista e dá ao
|
|
63
|
+
contêiner a coluna flexível. Novos elos ou folhas entram no SDK, com teste, e não no consumidor.
|
|
64
|
+
|
|
65
|
+
## Consequências
|
|
66
|
+
|
|
67
|
+
- `Page layout="fill"` › `PageBody` › `Content` › `ContentBody` › `ActionList` funciona sem classes
|
|
68
|
+
no consumidor, e a `Presentation` de lista em `fill` passa a entregar a rolagem interna prometida.
|
|
69
|
+
- Classes que o consumidor já escrevia para montar a cadeia continuam funcionando e podem ser
|
|
70
|
+
removidas: o `className` é mesclado por último e repete as mesmas classes.
|
|
71
|
+
- Uma `ActionList` sem `layout`, filha direta de um elo em uma página `fill`, passa a ocupar a altura.
|
|
72
|
+
Quem queria a lista crescendo com a página declara `layout="flow"`.
|
|
73
|
+
- A primeira renderização de uma cadeia com folha faz uma passada extra de layout effect. No
|
|
74
|
+
servidor, a cadeia só se forma na hidratação.
|
|
75
|
+
- `Tabs` e outros contêineres intermediários não são elos, em qualquer ponto do caminho. A lista só
|
|
76
|
+
confere o pai imediato: com um contêiner próprio entre dois elos (por exemplo, um `div` entre
|
|
77
|
+
`PageBody` e `Content`), ela ainda se registra e os elos viram coluna flexível, mas a altura não
|
|
78
|
+
atravessa o contêiner e a lista se comporta como `flow`. Quem compõe assim dá ao contêiner a
|
|
79
|
+
coluna flexível, como antes.
|
|
80
|
+
- O elo ativo vira coluna flexível com `align-items: stretch`, que a lista precisa. Os demais filhos
|
|
81
|
+
de cada elo ativo também são afetados: um botão inline se estica à largura do elo e margens
|
|
82
|
+
verticais deixam de colapsar. Para um irmão manter a largura do conteúdo, dê a ele `self-start`
|
|
83
|
+
ou envolva-o em um contêiner próprio.
|
|
84
|
+
- `Dialog` em `mode="alert"` também interrompe a cadeia, como os demais modos.
|
|
85
|
+
|
|
86
|
+
## Alternativas descartadas
|
|
87
|
+
|
|
88
|
+
### Manter a cadeia no consumidor
|
|
89
|
+
|
|
90
|
+
Não custa nada ao SDK. Foi descartada porque é o estado que produziu a documentação ensinando
|
|
91
|
+
classes de correção e a `Presentation` com `fill` que não chegava à lista.
|
|
92
|
+
|
|
93
|
+
### Aplicar a coluna flexível em todo elo dentro de `fill`
|
|
94
|
+
|
|
95
|
+
Mais simples: bastaria ler o contexto. Foi descartada porque duas seções irmãs com `flex-1` dividem
|
|
96
|
+
a altura ao meio e o conteúdo de uma transborda sobre a outra, e porque `flex-col` estica filhos
|
|
97
|
+
inline, como botões, que antes tinham a largura do conteúdo.
|
|
98
|
+
|
|
99
|
+
### Detectar a folha só por CSS (`:has`)
|
|
100
|
+
|
|
101
|
+
Evitaria o registro em React. Foi descartada porque `:has` enxerga qualquer descendente no DOM,
|
|
102
|
+
inclusive listas dentro de cards e contêineres que não repassam a altura, e porque a decisão da
|
|
103
|
+
lista — `fill` ou `flow` — ainda precisaria do contexto.
|
|
104
|
+
|
|
105
|
+
### Expor o contexto na API pública
|
|
106
|
+
|
|
107
|
+
Deixaria o consumidor criar elos próprios. Foi adiada: sem um caso real além de `ActionList`, o
|
|
108
|
+
contrato público seria especulativo.
|
|
109
|
+
|
|
110
|
+
## Verificação
|
|
111
|
+
|
|
112
|
+
- `tests/ui/fill-region.test.tsx` renderiza a cadeia `Page fill` › `PageBody` › `Content` ›
|
|
113
|
+
`ContentBody` › `ActionList` e confere atributo e classes de cada nível; cobre seções irmãs,
|
|
114
|
+
página `fill` sem lista, página `flow`, `layout="flow"` explícito, contêiner próprio com e sem
|
|
115
|
+
`fill` explícito, a interrupção por `Card` e a interrupção pela própria lista.
|
|
116
|
+
- `tests/ui/presentation.test.tsx` confere a `Presentation` de lista em `fill`, dentro e fora de
|
|
117
|
+
`PageShell`, e a lista em `flow` quando a `Presentation` é `flow`.
|
|
118
|
+
- A rolagem real é conferida em navegador: a happy-dom não calcula layout.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# ADR 0025 — O acabamento da moldura de Content não altera sua semântica
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-25.
|
|
5
|
+
- **Complementa:** ADRs 0021 e 0023.
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
`Content` separa o elemento semântico (`section` ou `fieldset`) da presença da moldura. Porém, o
|
|
10
|
+
acabamento `frameVariant="legend"`, que integra visualmente o título à borda, só está disponível
|
|
11
|
+
quando a raiz é um `fieldset`.
|
|
12
|
+
|
|
13
|
+
Essa restrição leva regiões somente de leitura a escolherem `fieldset` apenas para obter o
|
|
14
|
+
acabamento visual. É o caso de grupos de detalhes como Cliente e Veículo: eles formam seções
|
|
15
|
+
nomeadas com `DetailGroup`, não conjuntos de controles de formulário. A aparência desejada se
|
|
16
|
+
parece com uma legenda, mas não transforma o título em uma `legend` semântica.
|
|
17
|
+
|
|
18
|
+
As forças em tensão são:
|
|
19
|
+
|
|
20
|
+
- preservar `fieldset` e `legend` exclusivamente para grupos de campos relacionados;
|
|
21
|
+
- oferecer a mesma linguagem visual a regiões de leitura;
|
|
22
|
+
- manter título, mídia, descrição, contador e ações compatíveis com a anatomia normal de `Content`;
|
|
23
|
+
- não criar outra família pública apenas para desenhar uma borda interrompida.
|
|
24
|
+
|
|
25
|
+
## Decisão
|
|
26
|
+
|
|
27
|
+
`frameVariant="legend"` passa a ser aceito tanto por `Content as="section"` quanto por
|
|
28
|
+
`Content as="fieldset"`. A propriedade escolhe somente o acabamento da moldura; `as` continua
|
|
29
|
+
sendo a única propriedade que determina o elemento e sua semântica.
|
|
30
|
+
|
|
31
|
+
Em uma `section`, o título permanece um heading no nível declarado, o header continua aceitando
|
|
32
|
+
`ContentMedia`, `ContentMeta`, `ContentDescription` e `ContentActions`, e a borda é visualmente
|
|
33
|
+
interrompida ao redor das regiões do header que a ocupam. Nenhum elemento `legend` é emitido.
|
|
34
|
+
|
|
35
|
+
Em um `fieldset`, o título continua sendo a `legend` nativa e pode ser precedido pelo mesmo
|
|
36
|
+
`ContentMedia` usado em uma `section`. A mídia é decorativa e acompanha o nome do grupo; contador,
|
|
37
|
+
descrição e ações continuam fora da legenda. `as` altera a semântica, não a apresentação do par
|
|
38
|
+
mídia + título nem o comportamento nativo de formulários.
|
|
39
|
+
|
|
40
|
+
O nome `legend` descreve o acabamento visual existente e não cria um terceiro valor incompatível
|
|
41
|
+
para o mesmo desenho. A documentação deve apresentar primeiro a escolha semântica e depois o
|
|
42
|
+
acabamento, deixando explícito que uma região de leitura permanece `section`.
|
|
43
|
+
|
|
44
|
+
Para interromper a linha sem depender da transparência dos wrappers intermediários, Card, Dialog e
|
|
45
|
+
Drawer declaram `--surface-background` com o token que já usam (`--background` ou `--card`). As
|
|
46
|
+
regiões do header que cruzam a borda usam essa cor; quando não houver uma superfície declarada,
|
|
47
|
+
`Content` usa `--background` como fallback. O nome segue o papel da variável, como os demais
|
|
48
|
+
contratos CSS locais, sem prefixo de pacote. Uma superfície com fundo próprio que não seja uma
|
|
49
|
+
dessas três, como um `Card` com `bg-*`, um `Popover` ou um contêiner da aplicação, declara a mesma
|
|
50
|
+
variável com o fundo que pinta.
|
|
51
|
+
|
|
52
|
+
### Relação com a ADR 0020
|
|
53
|
+
|
|
54
|
+
A ADR 0020 mantém genéricos os slots de ícone, como `FieldLegend.icon`: o consumidor passa o glifo
|
|
55
|
+
quando quer só o ícone e compõe `IconTile` quando quer a moldura. `ContentMedia` não é um slot
|
|
56
|
+
genérico; é o slot de mídia da anatomia de superfícies (ADR 0023), que já promete a moldura, como
|
|
57
|
+
`CardMedia` e `ItemMedia variant="icon"`. Por isso a mídia de `Content` sempre desenha a moldura, e
|
|
58
|
+
`asChild` continua sendo a saída para uma marca com visual próprio.
|
|
59
|
+
|
|
60
|
+
Consumidores que já compunham `IconTile` antes deste slot existir não devem ganhar moldura dentro de
|
|
61
|
+
moldura. `ContentMedia` adota um `IconTile` recebido como filho: o slot assume o lugar dele e herda
|
|
62
|
+
`size`, `context`, `variant` e as demais props declaradas nele. `media` com `null` ou `false` é
|
|
63
|
+
tratado como ausência, e nenhuma moldura vazia é desenhada. Os ícones de
|
|
64
|
+
`advancedFilters.sectionIcons` da `ActionList` passam pelo mesmo slot, sem normalização própria.
|
|
65
|
+
|
|
66
|
+
## Consequências
|
|
67
|
+
|
|
68
|
+
- Regiões de leitura podem integrar o header à moldura sem declarar semântica de formulário.
|
|
69
|
+
- A composição `Content + DetailGroup` preserva `section`, heading, mídia, descrição, contador e
|
|
70
|
+
ações.
|
|
71
|
+
- Grupos de campos continuam usando `fieldset` e `legend` quando essa relação existe no conteúdo.
|
|
72
|
+
- Filtros e regiões de leitura usam o mesmo slot, escala e alinhamento para mídia e título.
|
|
73
|
+
- O default permanece `frameVariant="default"`; quem não usa as novas props não muda visualmente.
|
|
74
|
+
- `advancedFilters.sectionIcons` muda: um glifo solto, antes exibido sem moldura, passa a ocupar o
|
|
75
|
+
`ContentMedia` e ganha a moldura padrão. Um `IconTile` continua com a aparência que declara.
|
|
76
|
+
- A `legend` de todo `Content as="fieldset"` passa a envolver título e mídia nos wrappers do
|
|
77
|
+
`SurfaceHeader` (`legend > span[data-slot=content-heading] > …`). Seletores que dependiam do
|
|
78
|
+
título como filho direto da `legend` precisam ser ajustados.
|
|
79
|
+
- O acabamento precisa funcionar sobre as superfícies canônicas sem o consumidor reconstruir a
|
|
80
|
+
borda ou posicionar o header por classes próprias.
|
|
81
|
+
- Wrappers transparentes entre a superfície e `Content` não deixam a borda aparecer atrás do grupo
|
|
82
|
+
de heading ou das ações, porque a cor vem da variável declarada pela superfície e não do pai.
|
|
83
|
+
- Superfícies com fundo próprio fora de Card, Dialog e Drawer ficam responsáveis por declarar
|
|
84
|
+
`--surface-background`; sem isso, a faixa atrás do header usa `--background`.
|
|
85
|
+
|
|
86
|
+
## Alternativas consideradas
|
|
87
|
+
|
|
88
|
+
### Usar `fieldset` também para detalhes somente de leitura
|
|
89
|
+
|
|
90
|
+
Reaproveitaria a implementação atual, mas atribuiria semântica de grupo de formulário a uma lista
|
|
91
|
+
de definições. Foi descartada porque a escolha do elemento passaria a ser governada pela aparência.
|
|
92
|
+
|
|
93
|
+
### Criar outro componente de painel
|
|
94
|
+
|
|
95
|
+
Separaria esse desenho de `Content`, mas duplicaria a anatomia de região nomeada e voltaria a exigir
|
|
96
|
+
uma decisão entre estruturas equivalentes. Foi descartada porque `Content` já é responsável pelo
|
|
97
|
+
header, corpo e moldura da região.
|
|
98
|
+
|
|
99
|
+
### Criar um novo valor visual com o mesmo desenho
|
|
100
|
+
|
|
101
|
+
Um nome como `notched` evitaria a associação com a `legend`, mas criaria dois valores para o mesmo
|
|
102
|
+
acabamento e uma migração sem ganho observável. Foi descartado porque o contrato já afirma que
|
|
103
|
+
`frameVariant` não altera a semântica.
|
|
104
|
+
|
|
105
|
+
### Herdar o background do pai imediato
|
|
106
|
+
|
|
107
|
+
Manteria a implementação restrita a `Content`, mas `background-color: inherit` recebe
|
|
108
|
+
`transparent` quando `DrawerBody`, `CardBody` ou outro wrapper não pinta o próprio fundo. Foi
|
|
109
|
+
descartado porque o acabamento depende da superfície que efetivamente pinta, não do pai estrutural
|
|
110
|
+
mais próximo.
|
|
111
|
+
|
|
112
|
+
### Redesenhar a borda em segmentos
|
|
113
|
+
|
|
114
|
+
Evitaria cobrir a linha com uma cor, mas exigiria derivar segmentos antes, entre e depois de título,
|
|
115
|
+
descrição e ações com larguras dinâmicas. Foi descartado porque duplicaria no CSS a anatomia que o
|
|
116
|
+
header já organiza.
|
|
117
|
+
|
|
118
|
+
## Verificação
|
|
119
|
+
|
|
120
|
+
- Testes de UI confirmam que `Content as="section"` com `frameVariant="legend"` mantém `section`,
|
|
121
|
+
heading e todos os slots do header.
|
|
122
|
+
- Testes confirmam que o mesmo acabamento em `fieldset` continua emitindo `legend` e restringindo
|
|
123
|
+
seus slots.
|
|
124
|
+
- Testes confirmam que o default continua usando a moldura contínua nos dois elementos.
|
|
125
|
+
- Testes confirmam que Card, Dialog e Drawer declaram `--surface-background` e que o grupo de
|
|
126
|
+
heading e as ações do header usam essa variável, com `--background` como fallback. A herança da
|
|
127
|
+
variável através de wrappers transparentes é comportamento do CSS, não é provada pela happy-dom e
|
|
128
|
+
depende de conferência visual no catálogo, onde a página de `Content` a demonstra sobre um `Card`.
|
|
129
|
+
- Testes confirmam que um `IconTile` passado como mídia é adotado pelo slot, que `null` e `false`
|
|
130
|
+
não desenham mídia e que um glifo solto em `sectionIcons` ganha a moldura.
|
|
131
|
+
- Um teste confirma que `Page layout="fill"` leva a altura até a `ActionList` através de um
|
|
132
|
+
`Content` com `frameVariant="legend"` (ADR 0024).
|
|
133
|
+
- A documentação demonstra `Content + DetailGroup` no acabamento `legend` sem usar `fieldset` e
|
|
134
|
+
mantém `Content as="fieldset" + FieldGroup` para grupos de controles.
|
package/docs/data-products.md
CHANGED
|
@@ -10,7 +10,10 @@ Ele não é uma query, tabela ou view: essas são implementações possíveis. A
|
|
|
10
10
|
recorte e autorização continua sendo cada Action.
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
|
-
import { defineDataProduct, defineDomain } from '@softize/opus'
|
|
13
|
+
import { defineDataProduct, defineDomain } from '@softize/opus/core'
|
|
14
|
+
import { LeadEntity } from './entity.ts' // defineEntity({ name: 'lead', … })
|
|
15
|
+
import { leadList } from './actions/list.ts' // action 'lead.list'
|
|
16
|
+
import { leadFunnel } from './actions/funnel.ts' // action 'lead.funnel'
|
|
14
17
|
|
|
15
18
|
export const salesLeads = defineDataProduct({
|
|
16
19
|
id: 'sales.leads',
|
|
@@ -22,24 +25,27 @@ export const salesLeads = defineDataProduct({
|
|
|
22
25
|
classification: 'internal',
|
|
23
26
|
nature: 'real',
|
|
24
27
|
sources: [{ id: 'followize', label: 'Followize' }],
|
|
25
|
-
entities: ['
|
|
28
|
+
entities: ['lead'],
|
|
26
29
|
access: {
|
|
27
30
|
permissionContexts: ['sales'],
|
|
28
31
|
organizationalScopes: ['unit', 'team'],
|
|
29
32
|
},
|
|
30
|
-
interfaces: ['
|
|
33
|
+
interfaces: ['lead.list', 'lead.funnel'],
|
|
31
34
|
})
|
|
32
35
|
|
|
33
|
-
export const
|
|
34
|
-
name: '
|
|
35
|
-
entities: {
|
|
36
|
-
actions: {
|
|
36
|
+
export const leadDomain = defineDomain({
|
|
37
|
+
name: 'lead',
|
|
38
|
+
entities: { LeadEntity },
|
|
39
|
+
actions: { leadList, leadFunnel },
|
|
37
40
|
dataProducts: { salesLeads },
|
|
38
41
|
})
|
|
39
42
|
```
|
|
40
43
|
|
|
41
|
-
`interfaces` e `entities` usam os nomes declarados
|
|
42
|
-
|
|
44
|
+
`interfaces` e `entities` usam os nomes declarados — o `name` da action e o `name` de
|
|
45
|
+
`defineEntity` —, não a chave do objeto, o nome do arquivo ou da tabela. O registro do domínio e
|
|
46
|
+
`opus check` detectam relações inexistentes: aqui, `entities: ['LeadEntity']` ou `['Lead']` seriam
|
|
47
|
+
recusados, porque a entidade se declara como `lead`. As actions seguem o prefixo do domínio
|
|
48
|
+
(`lead.*`), como qualquer action registrada nele. `sources` identifica sistemas ou conjuntos
|
|
43
49
|
upstream; uma Fonte não precisa ser uma Entity Opus.
|
|
44
50
|
|
|
45
51
|
## Acesso e segurança
|
package/docs/protocol.md
CHANGED
|
@@ -1606,7 +1606,6 @@ defineReaction({
|
|
|
1606
1606
|
|
|
1607
1607
|
handler: async (ctx, event) => {
|
|
1608
1608
|
await syncToSalesforce(event.data.leadId)
|
|
1609
|
-
await ctx.emit('hub.lead.synced', { leadId: event.data.leadId })
|
|
1610
1609
|
},
|
|
1611
1610
|
})
|
|
1612
1611
|
|
|
@@ -1629,7 +1628,7 @@ type ReactionContext = {
|
|
|
1629
1628
|
user: User | null // ator que disparou o evento (pode ser system)
|
|
1630
1629
|
tenantId: string | null
|
|
1631
1630
|
db: unknown // mesmo data adapter
|
|
1632
|
-
emit: EmitFn //
|
|
1631
|
+
emit: EmitFn // descarta o evento sem publicar (sem encadeamento)
|
|
1633
1632
|
log: LoggerAdapter // logger pre-bound com contexto da reaction
|
|
1634
1633
|
storage: StorageAdapter | null // storage de arquivos (experimental)
|
|
1635
1634
|
provenance: Provenance // { kind: 'reaction', … }
|
|
@@ -1638,7 +1637,7 @@ type ReactionContext = {
|
|
|
1638
1637
|
}
|
|
1639
1638
|
```
|
|
1640
1639
|
|
|
1641
|
-
|
|
1640
|
+
Uma reaction não encadeia outra por evento: dentro dela, `ctx.emit` descarta o evento sem publicá-lo. Com `config.dev` ativo, cada descarte registra o aviso `runtime.reaction_emit_ignored` no log; em produção, o descarte é silencioso. Quando outro consumidor precisa desse fato, emita o evento no handler da action que o produz e declare-o em `emits`.
|
|
1642
1641
|
|
|
1643
1642
|
### Setup unificado
|
|
1644
1643
|
|
|
@@ -1690,7 +1689,7 @@ para cada reaction:
|
|
|
1690
1689
|
↓ (a) dedup check (se declarado)
|
|
1691
1690
|
↓ (b) authorize (se declarado)
|
|
1692
1691
|
↓ (c) handler executa (com timeout)
|
|
1693
|
-
↓ (d)
|
|
1692
|
+
↓ (d) ctx.emit dentro da reaction não publica (descartado, com aviso em dev)
|
|
1694
1693
|
↓ (e) audit (reaction completed/failed)
|
|
1695
1694
|
↓ (f) retry se falhou e config permite
|
|
1696
1695
|
```
|
package/docs/releasing.md
CHANGED
|
@@ -60,7 +60,7 @@ pra testar publicação seria a guarda atrapalhando quem está experimentando.
|
|
|
60
60
|
|
|
61
61
|
**Gate de qualidade**: depois do bump e da materialização, mas antes de publicar, roda
|
|
62
62
|
`pnpm typecheck` + `pnpm test` + `pnpm copy:check` + `base copy check` e **aborta a release
|
|
63
|
-
se qualquer um falhar**. O Opus declara `@softize/base ^2.
|
|
63
|
+
se qualquer um falhar**. O Opus declara `@softize/base ^2.4.0` em `dependencies`, pois usa
|
|
64
64
|
suas APIs públicas de filesystem em runtime, e materializa os artefatos Base no próprio repo;
|
|
65
65
|
a release não baixa uma política ad hoc. Como o
|
|
66
66
|
Opus **ship source** (`.ts`, sem build), essa é a última barreira antes do tarball — sem ela,
|
|
@@ -74,7 +74,7 @@ não é bloqueada por cobertura abaixo de 100%; a cobertura é acompanhada com `
|
|
|
74
74
|
**Smoke do esqueleto**: depois do bump e antes do publish, o `release.sh` gera um app com
|
|
75
75
|
`opus create`, instala o **tarball exato** que vai ser publicado e roda os gates dele
|
|
76
76
|
(typecheck · test · `opus check` · `opus copy --check` · `base copy check` · manifest ·
|
|
77
|
-
build). O template exige `@softize/base ^2.
|
|
77
|
+
build). O template exige `@softize/base ^2.4.0`; publique a Base compatível antes do Opus.
|
|
78
78
|
O `minimumReleaseAgeExclude` do template inclui os dois pacotes, e o smoke executa os
|
|
79
79
|
fragmentos de pre-push para provar que o layout pnpm instalado resolve ambos os CLIs.
|
|
80
80
|
É o que pega o que typecheck+test não
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softize/opus",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "20.1.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",
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
"@radix-ui/react-slot": "^1.1.1",
|
|
248
248
|
"@radix-ui/react-tabs": "^1.1.2",
|
|
249
249
|
"@radix-ui/react-tooltip": "^1.1.6",
|
|
250
|
-
"@softize/base": "^2.
|
|
250
|
+
"@softize/base": "^2.4.0",
|
|
251
251
|
"@tailwindcss/typography": "^0.5.20",
|
|
252
252
|
"@types/markdown-it": "^14.1.2",
|
|
253
253
|
"class-variance-authority": "^0.7.1",
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const KINDS = new Set(['simple', 'form', 'list', 'view'])
|
|
4
4
|
const cap = (value) => value.charAt(0).toUpperCase() + value.slice(1)
|
|
5
|
-
const
|
|
5
|
+
const camel = (value) => value.split('-').map((part, index) => (index === 0 ? part : cap(part))).join('')
|
|
6
|
+
const constant = (resource, verb) => camel(`${resource}-${verb}`)
|
|
6
7
|
|
|
7
8
|
function contractBody({ resource, verb, kind }) {
|
|
8
9
|
const name = `${resource}.${verb}`
|
package/src/core/runtime.ts
CHANGED
|
@@ -413,9 +413,35 @@ export class Runtime {
|
|
|
413
413
|
})
|
|
414
414
|
}
|
|
415
415
|
this.actions.set(action.name, action)
|
|
416
|
+
if (this.config.dev) this.warnIgnoredExecutionFields(action)
|
|
416
417
|
}
|
|
417
418
|
}
|
|
418
419
|
|
|
420
|
+
/**
|
|
421
|
+
* `emits` e `background` pertencem ao binding de qualquer kind pelo tipo, mas o runtime e o
|
|
422
|
+
* manifest só os aplicam em `simple` e `form`. Restringir o tipo quebraria bindings
|
|
423
|
+
* existentes; o aviso em desenvolvimento evita que a declaração pareça ter efeito.
|
|
424
|
+
*/
|
|
425
|
+
private warnIgnoredExecutionFields(action: ActionDef): void {
|
|
426
|
+
if (action.kind !== 'list' && action.kind !== 'view') return
|
|
427
|
+
const declared = action as { emits?: unknown; background?: unknown }
|
|
428
|
+
const fields = (['emits', 'background'] as const).filter((field) => declared[field] !== undefined)
|
|
429
|
+
if (fields.length === 0) return
|
|
430
|
+
const list = fields.map((field) => `\`${field}\``).join(' e ')
|
|
431
|
+
const effects = fields.map((field) =>
|
|
432
|
+
field === 'background'
|
|
433
|
+
? 'Sem fila, a action executa na própria requisição.'
|
|
434
|
+
: 'Os eventos declarados não entram no manifest nem na conferência do que o handler emite.',
|
|
435
|
+
)
|
|
436
|
+
this.log.warn(
|
|
437
|
+
`A action "${action.name}" é do kind "${action.kind}" e declara ${list}, que o runtime só ` +
|
|
438
|
+
`aplica em actions simple e form. ${effects.join(' ')} Remova ${list} da declaração da ` +
|
|
439
|
+
'action, no `bindAction` ou no `defineAction`; se a operação precisa de fila ou de ' +
|
|
440
|
+
'eventos declarados, use uma action simple ou form.',
|
|
441
|
+
{ code: 'runtime.action_field_ignored', action: action.name, kind: action.kind, fields },
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
419
445
|
private registerReactions(reactions: Array<ReactionDef<any>>): void {
|
|
420
446
|
for (const reaction of reactions) {
|
|
421
447
|
if (this.reactions.has(reaction.name)) {
|
|
@@ -455,6 +481,12 @@ export class Runtime {
|
|
|
455
481
|
})
|
|
456
482
|
}
|
|
457
483
|
|
|
484
|
+
// Antes de inicializar adapters: é uma checagem do registro, e falhar aqui não deixa
|
|
485
|
+
// conexão aberta para trás. Sem scheduler, a checagem não roda porque nenhum schedule
|
|
486
|
+
// executaria: os adapters inicializam e o start falha depois, com
|
|
487
|
+
// `runtime.eventbus_required` se também faltar event bus, senão `runtime.scheduler_required`.
|
|
488
|
+
if (this.scheduler !== undefined) this.assertScheduledActionsExist()
|
|
489
|
+
|
|
458
490
|
for (const adapter of this.allAdapters()) {
|
|
459
491
|
if (adapter.init !== undefined) await adapter.init(this)
|
|
460
492
|
if (adapter.dispose !== undefined) {
|
|
@@ -1203,6 +1235,35 @@ export class Runtime {
|
|
|
1203
1235
|
}
|
|
1204
1236
|
}
|
|
1205
1237
|
|
|
1238
|
+
/**
|
|
1239
|
+
* Um schedule com nome de action errado dispararia em silêncio para sempre. Schedules com
|
|
1240
|
+
* `enabled: false` ficam de fora: não são registrados e podem apontar para uma action que
|
|
1241
|
+
* ainda não existe neste ambiente.
|
|
1242
|
+
*/
|
|
1243
|
+
private assertScheduledActionsExist(): void {
|
|
1244
|
+
const missing = [...this.schedules.values()].filter(
|
|
1245
|
+
(schedule) => schedule.enabled !== false && !this.actions.has(schedule.action),
|
|
1246
|
+
)
|
|
1247
|
+
if (missing.length === 0) return
|
|
1248
|
+
const list = missing
|
|
1249
|
+
.map((schedule) => `"${schedule.name}" → "${schedule.action}"`)
|
|
1250
|
+
.join(', ')
|
|
1251
|
+
throw error({
|
|
1252
|
+
code: 'runtime.schedule_action_not_found',
|
|
1253
|
+
category: 'internal',
|
|
1254
|
+
message:
|
|
1255
|
+
`O runtime não iniciou porque ${
|
|
1256
|
+
missing.length === 1
|
|
1257
|
+
? 'um schedule aponta para uma action não registrada'
|
|
1258
|
+
: `${missing.length} schedules apontam para actions não registradas`
|
|
1259
|
+
}: ${list}. Corrija o nome em \`action\` ou registre a action no mesmo runtime; para ` +
|
|
1260
|
+
'manter o schedule sem executar, declare `enabled: false`.',
|
|
1261
|
+
meta: {
|
|
1262
|
+
schedules: missing.map((schedule) => ({ schedule: schedule.name, action: schedule.action })),
|
|
1263
|
+
},
|
|
1264
|
+
})
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1206
1267
|
private registerSchedule(schedule: ScheduleDef): void {
|
|
1207
1268
|
if (schedule.enabled === false) {
|
|
1208
1269
|
this.log.info('schedule disabled', { schedule: schedule.name })
|
|
@@ -1221,12 +1282,45 @@ export class Runtime {
|
|
|
1221
1282
|
typeof schedule.input === 'function'
|
|
1222
1283
|
? await (schedule.input as () => unknown | Promise<unknown>)()
|
|
1223
1284
|
: (schedule.input ?? {})
|
|
1224
|
-
await this.execute(schedule.action, input, {
|
|
1285
|
+
const result = await this.execute(schedule.action, input, {
|
|
1225
1286
|
user: null,
|
|
1226
1287
|
tenantId: null,
|
|
1227
1288
|
can: () => false,
|
|
1228
1289
|
provenance: { kind: 'schedule', scheduleName: schedule.name },
|
|
1229
1290
|
})
|
|
1291
|
+
if (result.ok) return
|
|
1292
|
+
// Ninguém espera o resultado de um disparo: sem este registro, a falha só existiria no
|
|
1293
|
+
// audit da action. Action inexistente não chega aqui, porque o start() recusa o schedule.
|
|
1294
|
+
this.log.error(
|
|
1295
|
+
`O schedule "${schedule.name}" não concluiu a action "${schedule.action}" (${result.error.code}). ` +
|
|
1296
|
+
'O agendamento continua ativo e o próximo disparo executa a action de novo. Confira a ' +
|
|
1297
|
+
'mensagem e o audit da action; `auth.unauthenticated` indica que a action agendada precisa ' +
|
|
1298
|
+
'declarar `public: true`.',
|
|
1299
|
+
{
|
|
1300
|
+
schedule: schedule.name,
|
|
1301
|
+
action: schedule.action,
|
|
1302
|
+
code: result.error.code,
|
|
1303
|
+
message: result.error.message,
|
|
1304
|
+
actionId: result.meta.actionId,
|
|
1305
|
+
...(result.error.cause !== undefined ? { cause: result.error.cause } : {}),
|
|
1306
|
+
},
|
|
1307
|
+
)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* `ctx.emit` de reaction não publica: encadear reactions por evento ainda não é suportado.
|
|
1312
|
+
* Fora de produção, o descarte vira aviso para que o evento não pareça ter sido entregue.
|
|
1313
|
+
*/
|
|
1314
|
+
private buildReactionEmit(reaction: ReactionDef<any>, log: Logger): ReactionContext['emit'] {
|
|
1315
|
+
return async (event) => {
|
|
1316
|
+
if (!this.config.dev) return
|
|
1317
|
+
log.warn(
|
|
1318
|
+
`A reaction "${reaction.name}" chamou ctx.emit('${event}'), mas reactions não publicam ` +
|
|
1319
|
+
'eventos: o evento foi descartado e nenhuma reaction ou integração o recebe. Para ' +
|
|
1320
|
+
'anunciar esse fato, emita-o no handler da action que o produz e declare-o em `emits`.',
|
|
1321
|
+
{ code: 'runtime.reaction_emit_ignored', reaction: reaction.name, event },
|
|
1322
|
+
)
|
|
1323
|
+
}
|
|
1230
1324
|
}
|
|
1231
1325
|
|
|
1232
1326
|
private async runReaction(
|
|
@@ -1260,7 +1354,7 @@ export class Runtime {
|
|
|
1260
1354
|
tenantId: event.tenant ?? null,
|
|
1261
1355
|
db: this.data?.contextExtension().db,
|
|
1262
1356
|
log: reactionLog,
|
|
1263
|
-
emit:
|
|
1357
|
+
emit: this.buildReactionEmit(reaction, reactionLog),
|
|
1264
1358
|
storage: this.storage ?? null,
|
|
1265
1359
|
cache: this.cache ?? null,
|
|
1266
1360
|
// Reação = contexto de sistema; o agente só alcança actions públicas (can nega por
|
|
@@ -1411,12 +1505,6 @@ function severityForError(err: any): 'info' | 'warning' | 'error' {
|
|
|
1411
1505
|
return 'error'
|
|
1412
1506
|
}
|
|
1413
1507
|
|
|
1414
|
-
/**
|
|
1415
|
-
* Placeholder pra `ctx.emit` em reactions. Reaction chain (reaction emite
|
|
1416
|
-
* eventos novos que disparam outras reactions) entra em v1+.
|
|
1417
|
-
*/
|
|
1418
|
-
const noopEmit: ActionContext['emit'] = async () => undefined
|
|
1419
|
-
|
|
1420
1508
|
function generateId(): string {
|
|
1421
1509
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
1422
1510
|
return crypto.randomUUID()
|
package/src/core/types.ts
CHANGED
|
@@ -255,8 +255,10 @@ export interface User {
|
|
|
255
255
|
export type CanFn = (permission: string, resource?: unknown) => boolean | Promise<boolean>
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
* Função que emite um `DomainEvent`.
|
|
259
|
-
* `ReactionContext.emit
|
|
258
|
+
* Função que emite um `DomainEvent`. Em `ActionContext.emit`, publica o evento pelo
|
|
259
|
+
* `EventBusAdapter`. Em `ReactionContext.emit`, descarta o evento sem publicá-lo, porque
|
|
260
|
+
* reactions ainda não encadeiam outras por evento; com `config.dev` ativo, cada descarte
|
|
261
|
+
* registra o aviso `runtime.reaction_emit_ignored`.
|
|
260
262
|
*/
|
|
261
263
|
export type EmitFn = <T = unknown>(
|
|
262
264
|
event: string,
|
|
@@ -293,6 +295,7 @@ export interface ReactionContext {
|
|
|
293
295
|
tenantId: string | null
|
|
294
296
|
db: unknown
|
|
295
297
|
log: Logger
|
|
298
|
+
/** Descarta o evento sem publicá-lo: reactions ainda não encadeiam outras. Ver `EmitFn`. */
|
|
296
299
|
emit: EmitFn
|
|
297
300
|
storage: StorageAdapter | null
|
|
298
301
|
cache: CacheAdapter | null
|