@softize/opus 25.6.0 → 25.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,123 +4,165 @@ title: Primeiros passos
4
4
 
5
5
  # Primeiros passos
6
6
 
7
- O Opus ajuda cliente e servidor a preservar as mesmas regras à medida que uma aplicação
8
- evolui. Entidades e contratos de action ficam em uma camada compartilhada; o runtime, a UI e
9
- as ferramentas usam essas definições sem exigir schemas e tipos paralelos.
7
+ Vamos calcular o preço de uma assinatura a partir do tamanho da equipe. Ao terminar, você terá
8
+ uma operação com entrada validada, um handler no servidor e um teste que confirma a regra.
10
9
 
11
- ## O modelo
10
+ Neste exemplo, cada pessoa custa R$ 29 por mês. A consulta é pública, não grava dados e não
11
+ precisa de banco. Isso permite acompanhar o caminho completo de uma action antes de configurar
12
+ um servidor HTTP ou uma interface.
12
13
 
13
- As regras de uma aplicação costumam aparecer em vários lugares: validação, transporte, API,
14
- formulários e documentação. No Opus, entidades, dicionários e contratos de action descrevem essas
15
- regras uma vez. A API e a web consomem a mesma definição, enquanto runtime, adapters, UI, CLI
16
- e verificações ajudam a mantê-la consistente. Cada projeto acrescenta seu domínio sobre essa
17
- base versionada.
14
+ ## Prepare o projeto
18
15
 
19
- ## Começar do zero
20
-
21
- Para um projeto novo, use `opus create`: ele gera o app com os pré-requisitos do protocolo, da UI e
22
- do preview já configurados, e os gates passam desde o início.
23
-
24
- O Opus e a Base ficam no npm público: a instalação não exige token nem configuração de escopo.
25
- Crie o projeto e materialize as duas camadas antes dos gates:
16
+ Se você já trabalha em um projeto Opus, siga para o contrato. Para criar um projeto novo, use um
17
+ ambiente com Node.js e pnpm instalados:
26
18
 
27
19
  ```bash
28
20
  pnpm dlx @softize/opus create meu-app
29
- cd meu-app && git init && pnpm install && pnpm run setup && pnpm test
21
+ cd meu-app
22
+ git init
23
+ pnpm install
24
+ pnpm run setup
25
+ pnpm test
30
26
  ```
31
27
 
32
- O comando cria a estrutura inicial e instala a versão estável disponível. Em uma máquina nova,
33
- se o `dlx` informar que o diretório global de binários não está no `PATH`, execute `pnpm setup`
34
- e abra outra sessão do terminal. Se o setup encontrar uma seção antiga do pnpm no arquivo de
35
- inicialização do shell (`ERR_PNPM_BAD_SHELL_SECTION`), `pnpm setup --force` substitui somente
36
- esse bloco.
28
+ O projeto inclui TypeScript, Vite, React, um domínio de tarefas e os comandos de teste e
29
+ verificação. `setup` materializa as instruções e os artefatos do Opus e da Base usados pelo
30
+ projeto. Para adotar em um aplicativo existente, criar um monorepo ou resolver uma falha de
31
+ instalação, consulte [Instalação](/docs/installation).
32
+
33
+ ## Declare o contrato
34
+
35
+ O contrato diz o que a operação recebe, o que devolve e quem pode chamá-la. Ele fica em um arquivo
36
+ que cliente e servidor conseguem importar, sem banco, segredo ou driver de servidor.
37
37
 
38
- Durante as primeiras 24 horas de uma release, a quarentena do pnpm pode resolver silenciosamente
39
- uma versão anterior. Se o CLI responder com “Comando desconhecido”, informe a versão explicitamente
40
- com `pnpm dlx @softize/opus@<versão> create meu-app` ou desative a quarentena apenas nessa chamada
41
- com `pnpm --config.minimum-release-age=0 dlx …`. Dentro do projeto, o workspace exclui Opus e Base
42
- da quarentena para preservar o par validado.
38
+ Crie `src/domains/subscription/actions/quote.contract.ts`:
43
39
 
44
- O app criado inclui:
40
+ ```ts
41
+ import { defineContract } from '@softize/opus/core'
42
+ import { z } from 'zod'
43
+
44
+ export const subscriptionQuoteContract = defineContract({
45
+ name: 'subscription.quote',
46
+ kind: 'simple',
47
+ summary: 'Calcula a mensalidade de uma equipe',
48
+ description: 'A assinatura custa R$ 29 por pessoa por mês, com pelo menos uma pessoa.',
49
+ input: z.object({ seats: z.number().int().positive() }),
50
+ output: z.object({ monthlyTotalInCents: z.number().int().nonnegative() }),
51
+ effect: 'read',
52
+ personalData: false,
53
+ public: true,
54
+ })
55
+ ```
45
56
 
46
- - `opus.config.ts` e um domínio de exemplo documentado, sem violações no `opus check`;
47
- - Vite, React e o tema do Opus sobre Tailwind v4, configurado por CSS;
48
- - um dev server que respeita a variável `PORT`, para que um orquestrador de preview escolha a
49
- porta;
50
- - `opus.json`, com a versão adotada, e as instruções, skills e hooks gerenciados.
57
+ `seats` aceita somente um inteiro positivo. O valor devolvido usa centavos: três pessoas custam
58
+ `8700`, ou R$ 87. `public: true` permite consultar o preço sem login; quando esse campo é omitido,
59
+ a action exige uma pessoa autenticada.
51
60
 
52
- A Base cuida do método geral de trabalho, da memória e da revisão; o Opus acrescenta o conhecimento
53
- específico do SDK.
61
+ O nome `subscription.quote` identifica a operação. O `kind: 'simple'` a define como uma chamada
62
+ sem formulário, coleção ou página de detalhe. Os outros tipos estão em
63
+ [Actions e contratos](/docs/actions).
54
64
 
55
- Em um monorepo, crie primeiro a raiz com `--monorepo` e depois cada app dentro dela. O `create`
56
- detecta o `pnpm-workspace.yaml` e gera só os arquivos do app, sem um workspace aninhado. O que a raiz
57
- precisa ter, como o glob de packages, `allowBuilds` e as exclusões de quarentena para Opus e Base,
58
- aparece como aviso, sem alterar os arquivos da raiz:
65
+ ## Ligue a regra ao contrato
59
66
 
60
- ```bash
61
- pnpm dlx @softize/opus create meu-cliente --monorepo # a raiz do workspace
62
- cd meu-cliente && git init
63
- pnpm dlx @softize/opus create apps/portal # o app (modo detectado)
67
+ O handler recebe a entrada validada e calcula a resposta. `bindAction` liga esse código ao
68
+ contrato sem colocar a implementação no módulo que o cliente importa.
69
+
70
+ Crie `src/domains/subscription/actions/quote.ts`:
71
+
72
+ ```ts
73
+ import { bindAction } from '@softize/opus/core'
74
+ import { subscriptionQuoteContract } from './quote.contract.ts'
75
+
76
+ export const subscriptionQuote = bindAction(subscriptionQuoteContract, {
77
+ handler: async (_ctx, input) => ({
78
+ monthlyTotalInCents: input.seats * 2900,
79
+ }),
80
+ })
64
81
  ```
65
82
 
66
- ## Carregar o Opus em um projeto existente
83
+ O contrato permanece compartilhável. O binding fica do lado do servidor, onde também poderão
84
+ entrar banco, integrações e outras dependências da regra.
67
85
 
68
- O `opus.json` registra a versão do Opus adotada pelo app; se ele ainda não existe, o setup o cria.
69
- `opus setup` roda em cada app e grava o marcador, o inventário de copy e as projeções específicas do
70
- SDK. `base setup` materializa na raiz do repositório o método geral, a memória, os agentes e a
71
- revisão. Os componentes vêm de `@softize/opus/ui/react`, e o tema, de um arquivo CSS.
86
+ ## Confira o resultado e o limite da entrada
72
87
 
73
- ```bash
74
- # Bootstrap idempotente de um projeto existente.
75
- pnpm add @softize/opus
76
- pnpm add -D @softize/base
77
- pnpm exec opus setup
78
- pnpm exec base setup
88
+ `runAction` executa a action pela fronteira do contrato. Assim, o teste verifica tanto a conta
89
+ quanto a validação que ocorre antes do handler.
90
+
91
+ Crie `src/domains/subscription/actions/quote.test.ts`:
92
+
93
+ ```ts
94
+ import { runAction } from '@softize/opus/testing'
95
+ import { expect, it } from 'vitest'
96
+ import { subscriptionQuote } from './quote.ts'
97
+
98
+ it('calcula a assinatura de três pessoas', async () => {
99
+ const { output } = await runAction(subscriptionQuote, { seats: 3 })
100
+ expect(output).toEqual({ monthlyTotalInCents: 8700 })
101
+ })
102
+
103
+ it('recusa uma equipe sem pessoas', async () => {
104
+ await expect(runAction(subscriptionQuote, { seats: 0 })).rejects.toMatchObject({
105
+ code: 'validation.invalid_input',
106
+ })
107
+ })
79
108
  ```
80
109
 
81
- ```css
82
- /* index.css do app — o tema do Opus e os componentes do pacote no scan do Tailwind. */
83
- @import '@softize/opus/ui/theme.css';
84
- @source '../node_modules/@softize/opus/src/ui/**/*.{ts,tsx}';
110
+ Execute `pnpm test`. Os dois testes devem passar. Não foi necessário criar um servidor, simular
111
+ login ou configurar banco: o teste usa a operação ligada ao seu handler.
112
+
113
+ ## Inclua a action no domínio
114
+
115
+ Um domínio reúne as operações e os dados de um assunto do produto. Registre a nova action em
116
+ `src/domains/subscription/index.ts`:
117
+
118
+ ```ts
119
+ import { defineDomain } from '@softize/opus/core'
120
+ import { subscriptionQuote } from './actions/quote.ts'
121
+
122
+ export const subscriptionDomain = defineDomain({
123
+ name: 'subscription',
124
+ description: 'Assinaturas de equipes e cálculo de mensalidade.',
125
+ actions: { subscriptionQuote },
126
+ })
85
127
  ```
86
128
 
87
- ## Montar o runtime
129
+ No `opus.config.ts`, importe `subscriptionDomain` e acrescente-o ao array `domains` que já existe.
130
+ Essa lista informa à CLI quais domínios entram no manifest gerado pelo projeto.
88
131
 
89
- No servidor, `createRuntime` reúne as capacidades que o app usa. Cada capacidade, como banco,
90
- autenticação ou auditoria, é um adapter que pode ser trocado sem alterar o domínio. Os providers da
91
- interface estão em [Como consumir](/ui/como-consumir).
132
+ Para executar a action no runtime, registre o mesmo domínio. Este trecho pode rodar em um
133
+ arquivo do servidor ou em um teste de integração:
92
134
 
93
135
  ```ts
94
136
  import { createRuntime } from '@softize/opus/core'
95
- import { jwtAuth } from '@softize/opus/auth/jwt'
96
- import { kyselyData } from '@softize/opus/data/kysely'
97
- import { fastifyServer } from '@softize/opus/server/fastify'
98
- import { pgAudit } from '@softize/opus/audit/pg'
99
- import { workspaceDomain } from './domains/workspace/index.ts'
100
-
101
- const runtime = createRuntime({
102
- server: fastifyServer({ app }),
103
- data: kyselyData({ db }), // Kysely e Postgres
104
- auth: jwtAuth({ secret: process.env.JWT_SECRET!, mapUser: (p) => ({ id: String(p.sub) }) }),
105
- audit: [pgAudit({ pool })], // trilha de auditoria
106
- config: { env, i18n: { defaultLocale: 'pt-BR' } },
137
+ import { subscriptionDomain } from './domains/subscription/index.ts'
138
+
139
+ const runtime = createRuntime()
140
+ runtime.register([subscriptionDomain])
141
+
142
+ const result = await runtime.execute('subscription.quote', { seats: 3 }, {
143
+ user: null,
144
+ tenantId: null,
145
+ can: () => false,
107
146
  })
108
147
 
109
- // Só o que foi registrado existe para o runtime.
110
- runtime.register([workspaceDomain])
111
- await runtime.start()
148
+ if (result.ok) {
149
+ console.log(result.data) // { monthlyTotalInCents: 8700 }
150
+ }
112
151
  ```
113
152
 
114
- Cada capacidade e o registro de domínios estão detalhados em [Runtime & adapters](/docs/runtime).
153
+ O runtime devolve um resultado com `ok` para distinguir sucesso de falha. A consulta continua
154
+ pública mesmo sem usuário. Uma action que não foi registrada não pode ser executada.
155
+
156
+ ## Continue a partir do que você precisa construir
115
157
 
116
- ## Antes de alterar o projeto
158
+ Você já declarou, executou e testou uma action. O próximo passo depende de onde a operação será
159
+ usada:
117
160
 
118
- A documentação do próprio repositório descreve o que é específico dele; esta página cobre só o que
119
- vale para todo projeto Opus.
161
+ - Para publicar a operação por HTTP e acrescentar banco ou autenticação, siga para
162
+ [Runtime e adapters](/docs/runtime).
163
+ - Para criar um formulário, uma coleção ou um detalhe, veja os tipos em
164
+ [Actions e contratos](/docs/actions) e os [componentes de UI](/ui/como-consumir).
165
+ - Para cobrir permissões e efeitos da operação, avance para [Testar actions](/docs/testing).
120
166
 
121
- - O `CLAUDE.md` do repositório e de cada app reúne o mapa do código, os gates e os cuidados locais.
122
- - A `description` de entidades e actions registra a regra de negócio, que `opus gen` projeta no
123
- manifest.
124
- - Antes de entregar, rode `opus check` (convenções das actions e da UI), `opus copy --check` e
125
- `base copy check` (inventário e política de copy) e, com banco, `opus db check` (divergência entre
126
- entidades e banco).
167
+ Antes de entregar uma mudança, execute `pnpm exec opus check`, `pnpm exec opus copy --check`
168
+ e os gates do seu repositório. Consulte [CLI](/docs/cli) para entender o que cada comando verifica.
@@ -0,0 +1,94 @@
1
+ ---
2
+ title: Instalação
3
+ ---
4
+
5
+ # Instalação
6
+
7
+ Use `opus create` para começar um projeto com TypeScript, React, testes e as verificações do
8
+ Opus configurados. Se o aplicativo já existe, instale o pacote e execute o setup para acrescentar
9
+ os artefatos do SDK ao repositório.
10
+
11
+ Os comandos desta página usam pnpm. Os pacotes Opus e Base são públicos e não exigem token de
12
+ registro.
13
+
14
+ ## Criar um aplicativo
15
+
16
+ ```bash
17
+ pnpm dlx @softize/opus create meu-app
18
+ cd meu-app
19
+ git init
20
+ pnpm install
21
+ pnpm run setup
22
+ pnpm test
23
+ ```
24
+
25
+ O aplicativo inclui um domínio de tarefas, `opus.config.ts`, Vite, React, o tema do Opus e os
26
+ comandos de teste e verificação. O dev server respeita a variável `PORT`. `opus.json` registra a
27
+ versão do SDK adotada pelo app; `setup` materializa as instruções, skills e hooks gerenciados.
28
+
29
+ Quando os testes passarem, continue em [Primeiros passos](/docs/getting-started).
30
+
31
+ ## Criar um monorepo
32
+
33
+ Crie a raiz do workspace antes dos aplicativos:
34
+
35
+ ```bash
36
+ pnpm dlx @softize/opus create meu-cliente --monorepo
37
+ cd meu-cliente
38
+ git init
39
+ pnpm dlx @softize/opus create apps/portal
40
+ pnpm install
41
+ pnpm --dir apps/portal run setup
42
+ ```
43
+
44
+ Dentro de um workspace pnpm, `create` gera somente os arquivos do aplicativo. Ele detecta o
45
+ `pnpm-workspace.yaml` da raiz e evita criar um workspace aninhado.
46
+
47
+ Se a raiz já existir, o comando avisa quando faltar configuração para incluir o app em
48
+ `packages`, permitir o postinstall com `allowBuilds` ou excluir Opus e Base da quarentena de
49
+ versões. Esses avisos não alteram os arquivos da raiz.
50
+
51
+ ## Adotar em um projeto existente
52
+
53
+ Na pasta do aplicativo, instale o SDK e execute o setup do Opus. Execute o setup da Base na raiz
54
+ do repositório:
55
+
56
+ ```bash
57
+ pnpm add @softize/opus
58
+ pnpm add -D @softize/base
59
+ pnpm exec opus setup
60
+ pnpm exec base setup
61
+ ```
62
+
63
+ Os dois setups são idempotentes. O Opus grava o marcador `opus.json`, o inventário de copy e as
64
+ projeções específicas do SDK. A Base materializa o método geral de trabalho, a memória e os
65
+ agentes. Em um monorepo, cada aplicativo mantém seu próprio marcador e sua configuração Opus.
66
+
67
+ Para usar a UI em um aplicativo com Tailwind v4, importe o tema e inclua os componentes do
68
+ pacote no scan do Tailwind. Este exemplo considera um CSS dentro de `src/`:
69
+
70
+ ```css
71
+ @import '@softize/opus/ui/theme.css';
72
+ @source '../node_modules/@softize/opus/src/ui/**/*.{ts,tsx}';
73
+ ```
74
+
75
+ Os componentes são importados de `@softize/opus/ui/react`. A configuração dos providers e da
76
+ execução de actions está em [Como consumir](/ui/como-consumir).
77
+
78
+ ## Resolver uma falha antes da criação
79
+
80
+ Se o pnpm informar que o diretório global de binários não está no `PATH`, execute `pnpm setup`
81
+ e abra uma nova sessão do terminal. Quando o setup apontar `ERR_PNPM_BAD_SHELL_SECTION`,
82
+ `pnpm setup --force` substitui o bloco antigo do pnpm no arquivo de inicialização do shell.
83
+
84
+ Uma política de quarentena do pnpm pode resolver uma versão anterior durante as primeiras
85
+ horas de uma release. Se `create` responder com “Comando desconhecido”, informe a versão que
86
+ você quer usar:
87
+
88
+ ```bash
89
+ pnpm dlx @softize/opus@<versão> create meu-app
90
+ ```
91
+
92
+ Outra opção é desativar a quarentena somente nessa chamada com
93
+ `pnpm --config.minimum-release-age=0 dlx @softize/opus create meu-app`.
94
+ O workspace gerado exclui Opus e Base dessa quarentena para manter o par adotado pelo template.
@@ -57,8 +57,8 @@ entrada. Em `_meta`, cada tool traz os
57
57
  identificadores e `com.softize.opus/data-product-labels`, o nome legível de cada um.
58
58
 
59
59
  Quando a action declara o que faz com os dados, a tool traz também as anotações padrão do MCP:
60
- `readOnlyHint` vem de `effect`. Numa action que escreve, `destructiveHint` vai sempre explícito, a
61
- partir de `ai.destructive`, porque o MCP trata a ausência numa escrita como destrutiva.
60
+ `readOnlyHint` vem de `effect`. Em uma action que escreve, `destructiveHint` vai sempre explícito, a
61
+ partir de `ai.destructive`, porque o MCP trata a ausência em uma escrita como destrutiva.
62
62
  Em `_meta`, `com.softize.opus/effect` repete o efeito e `com.softize.opus/personal-data` diz se a
63
63
  resposta contém dado pessoal. Na action dona de um tipo de recurso, `com.softize.opus/assistant`
64
64
  traz o assistente declarado, para o host conhecê-lo pelo catálogo que já consulta e aplicar à saída
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Visão geral
3
+ ---
4
+
5
+ # Opus
6
+
7
+ Opus é um SDK para aplicações TypeScript. Você declara as operações do produto em contratos
8
+ compartilhados, liga a implementação no servidor e usa essas mesmas definições no cliente,
9
+ nos testes e nas ferramentas do projeto.
10
+
11
+ Uma consulta de mensalidade, por exemplo, recebe o tamanho da equipe e devolve o preço. O
12
+ contrato define o formato dessas duas informações e permite o acesso sem login. O handler
13
+ calcula o valor. A interface importa o contrato para chamar a operação com os mesmos tipos
14
+ e validações usados no servidor.
15
+
16
+ ## O que você precisa entender primeiro
17
+
18
+ Uma **action** é uma operação do sistema: consultar um preço, listar tarefas, salvar um cadastro
19
+ ou ler um registro. Cada action tem um nome, uma entrada e uma saída.
20
+
21
+ O **contrato** é a parte compartilhada da action. Ele declara o formato dos dados e as informações
22
+ que seus consumidores precisam conhecer. O **binding** acrescenta o handler que executa a regra
23
+ no servidor. Essa divisão permite importar o contrato na interface sem levar banco ou segredo
24
+ para o navegador.
25
+
26
+ O **domínio** agrupa actions e dados de um assunto do produto. O **runtime** executa o que foi
27
+ registrado e oferece as dependências configuradas pelo aplicativo, como autenticação, banco ou
28
+ auditoria. Essas dependências entram por adapters; você escolhe as peças necessárias ao projeto.
29
+
30
+ ```text
31
+ Contrato compartilhado ──→ Cliente e interface
32
+ │
33
+ └── + handler ──→ Action registrada no runtime
34
+ ```
35
+
36
+ Você escreve a regra de negócio e monta o aplicativo. O Opus fornece as declarações, a execução,
37
+ os componentes de interface e as verificações que trabalham sobre essas declarações.
38
+
39
+ ## Faça uma primeira operação
40
+
41
+ Em [Primeiros passos](/docs/getting-started), você calcula uma mensalidade, valida a entrada e
42
+ testa o resultado. O exemplo usa uma regra pequena e dispensa banco, servidor HTTP e login.
43
+
44
+ Depois desse percurso, [Actions e contratos](/docs/actions) explica como a mesma estrutura atende
45
+ a formulários, listas, detalhes e operações com autorização.
46
+
47
+ ## Continue o trabalho que trouxe você até aqui
48
+
49
+ | Você precisa… | Comece por… |
50
+ | --- | --- |
51
+ | Criar uma operação ou mudar uma regra | [Actions e contratos](/docs/actions) |
52
+ | Publicar uma API e conectar dependências | [Runtime e adapters](/docs/runtime) |
53
+ | Modelar dados persistidos | [Camada de dados](/docs/data) |
54
+ | Construir uma tela | [Como consumir](/ui/como-consumir) |
55
+ | Testar uma operação e suas permissões | [Testes de actions](/docs/testing) |
56
+ | Atualizar a versão usada no projeto | [Atualização do Opus](/docs/upgrading) |
57
+
58
+ O catálogo de [UI](/ui) tem navegação própria pelo volume de componentes e exemplos visuais.
@@ -69,6 +69,10 @@ render(
69
69
  Use `SidebarNav` quando grupos e itens já puderem ser descritos como dados. Use `SidebarItem`
70
70
  diretamente quando a composição precisar de controles, badges ou hierarquia particular.
71
71
 
72
+ `SidebarItem` aceita `href` para destinos nativos, mantendo foco e abertura em outra aba.
73
+ Quando o host usa um roteador, forneça também `onClick`: somente o clique simples é interceptado;
74
+ cliques com modificadores preservam o comportamento do link. Sem `href`, o item continua sendo um botão.
75
+
72
76
  ## Colapso
73
77
 
74
78
  `collapsed` informa o estado compacto aos itens. Forneça labels acessíveis mesmo quando somente os
@@ -7,7 +7,14 @@
7
7
  * `@softize/opus/ui/docs/standalone` — não passa por aqui.
8
8
  */
9
9
  export { DocBrowser } from './CatalogDocBrowser.tsx'
10
+ export { DocHeader } from './DocHeader.tsx'
11
+ export { DocCatalog, DocRecordDetail } from './DocCatalog.tsx'
12
+ export type { DocCatalogProps, DocRecordDetailProps } from './DocCatalog.tsx'
13
+ export { decisionRecord, releaseRecords, parseChangelog, filterDocumentationRecords } from './records.ts'
14
+ export type { DocumentationRecord } from './records.ts'
15
+ export type { DocHeaderProps } from './DocHeader.tsx'
10
16
  export type { DocBrowserProps } from './CatalogDocBrowser.tsx'
17
+ export type { DocBrowserSearchOptions } from './DocBrowser.tsx'
11
18
  export { DOC_SECTIONS, PROTOCOL_SECTIONS, UI_SECTIONS } from './registry.tsx'
12
19
  export type { DocEntry, DocGroup, DocSection } from './types.ts'
13
20
  export { docSectionsFromFolder } from './catalog-folder.tsx'
@@ -17,5 +24,5 @@ export { Example, CodeBlock, DocPage } from './doc.tsx'
17
24
  // Renderer de markdown do DocBrowser: prosa + fence ```tsx preview vira componente vivo.
18
25
  export { DocMarkdown } from './markdown.tsx'
19
26
  export type { DocMarkdownProps } from './markdown.tsx'
20
- // O changelog do pacote, pronto pra montar (o site o usa como página do menu superior).
27
+ // O changelog do pacote, pronto para montar em um destino global ou no catálogo embutido.
21
28
  export { OpusChangelog, ChangelogView } from './changelog.tsx'
@@ -88,9 +88,11 @@ export interface DocMarkdownProps {
88
88
  /** Reescreve links do Markdown antes da renderização. Útil para resolver caminhos relativos
89
89
  * quando o documento é publicado sob uma rota do aplicativo. */
90
90
  resolveHref?: (href: string) => string
91
+ /** Mantém código didático aberto quando false; exemplos vivos preservam sua composição. */
92
+ codeCollapsible?: boolean
91
93
  }
92
94
 
93
- export function DocMarkdown({ content, scope, resolveHref }: DocMarkdownProps): React.ReactElement {
95
+ export function DocMarkdown({ content, scope, resolveHref, codeCollapsible = true }: DocMarkdownProps): React.ReactElement {
94
96
  // O `scope` ESTENDE o default (não substitui): o pattern injeta o palco doc-client
95
97
  // (DocBrowserActionProvider + contratos) sem perder o opus/ui + lucide + hooks. Memoiza pra
96
98
  // não re-avaliar os LiveBlocks a cada render (o scope injetado é estável no chamador).
@@ -135,7 +137,7 @@ export function DocMarkdown({ content, scope, resolveHref }: DocMarkdownProps):
135
137
  {/\b(preview|live)\b/.test(info) ? (
136
138
  <LiveBlock code={t.content} scope={liveScope} previewClassName={previewClassName} />
137
139
  ) : (
138
- <CodeBlock code={t.content.replace(/\n$/, '')} lang={lang} />
140
+ <CodeBlock code={t.content.replace(/\n$/, '')} lang={lang} collapsible={codeCollapsible} />
139
141
  )}
140
142
  </div>,
141
143
  )
@@ -144,5 +146,5 @@ export function DocMarkdown({ content, scope, resolveHref }: DocMarkdownProps):
144
146
  }
145
147
  }
146
148
  flush()
147
- return <div className="prose prose-sm max-w-none space-y-4">{parts}</div>
149
+ return <div data-slot="markdown" className="prose prose-sm max-w-none space-y-4">{parts}</div>
148
150
  }
@@ -0,0 +1,144 @@
1
+ export interface DocumentationRecord {
2
+ id: string;
3
+ title: string;
4
+ href: string;
5
+ date?: string;
6
+ status?: string;
7
+ searchText: string;
8
+ body: string;
9
+ summary?: string;
10
+ content?: string;
11
+ breaking?: string;
12
+ afterBreaking?: string;
13
+ }
14
+
15
+ export interface ChangelogVersion {
16
+ version: string;
17
+ date?: string;
18
+ body: string;
19
+ breaking?: string;
20
+ beforeBreaking?: string;
21
+ afterBreaking?: string;
22
+ /** Corpo original, com a ordem das seções preservada para a leitura integral. */
23
+ sourceBody?: string;
24
+ }
25
+
26
+ export interface ParsedChangelog {
27
+ intro: string;
28
+ versions: ChangelogVersion[];
29
+ }
30
+
31
+ /** Preserva a convenção publicada: `## versão — data` e seção `### Breaking`. */
32
+ export function parseChangelog(source: string): ParsedChangelog {
33
+ const chunks = `\n${source.trim()}`.split(/\n## /);
34
+ const versions = chunks.slice(1).map((chunk) => {
35
+ const nl = chunk.indexOf("\n");
36
+ const header = (nl === -1 ? chunk : chunk.slice(0, nl)).trim();
37
+ const rest = nl === -1 ? "" : chunk.slice(nl + 1);
38
+ const [version, date] = header.split(/\s+—\s+/);
39
+ const sourceBody = rest.trim();
40
+ const breakingSection =
41
+ /^### Breaking[\t ]*\r?\n([\s\S]*?)(?=^###[\t ]|(?![\s\S]))/m.exec(sourceBody);
42
+ const body = breakingSection
43
+ ? sourceBody.slice(0, breakingSection.index) +
44
+ sourceBody.slice(breakingSection.index + breakingSection[0].length)
45
+ : sourceBody;
46
+ return {
47
+ version: (version ?? header).trim(),
48
+ ...(date !== undefined ? { date: date.trim() } : {}),
49
+ body: (body ?? "").trim(),
50
+ sourceBody,
51
+ ...(breakingSection
52
+ ? {
53
+ breaking: breakingSection[1]!.trim(),
54
+ beforeBreaking: sourceBody.slice(0, breakingSection.index).trim(),
55
+ afterBreaking: sourceBody
56
+ .slice(breakingSection.index + breakingSection[0].length)
57
+ .trim(),
58
+ }
59
+ : {}),
60
+ };
61
+ });
62
+ return { intro: chunks[0]?.trim() ?? "", versions };
63
+ }
64
+
65
+ /** Extrai só os campos declarados; a ausência de metadata não vira uma decisão presumida. */
66
+ export function decisionRecord(
67
+ source: string,
68
+ href: string,
69
+ ): DocumentationRecord | undefined {
70
+ const heading = source.match(/^#\s+ADR\s+(\d+)\s*[—–:-]\s*(.+)$/m);
71
+ if (!heading) return undefined;
72
+ const header = source.split(/^##\s/m)[0] ?? "";
73
+ const metadata = (name: string): string | undefined => {
74
+ const value = header.match(
75
+ new RegExp(`^[\\t ]*-?[\\t ]*\\*\\*${name}:?\\*\\*:?[\\t ]*(.+)$`, "mi"),
76
+ )?.[1];
77
+ return value?.trim().replace(/\.$/, "");
78
+ };
79
+ const declaredStatus = metadata("Status");
80
+ // O estado resume a metadata; ressalvas e andamento permanecem no texto integral da decisão.
81
+ const status =
82
+ declaredStatus?.match(
83
+ /^(parcialmente (?:substituída|supersedida)|substituída|supersedida|aceita(?: para (?:protótipo|implementação(?: local)?|candidato local)| como primeiro corte)?|proposta|implementada|rejeitada)\b/i,
84
+ )?.[1] ?? declaredStatus;
85
+ const context = source
86
+ .match(/^##[\t ]+Contexto\b[^\n]*\n([\s\S]*)/im)?.[1]
87
+ ?.split(/^##[\t ]/m)[0];
88
+ return {
89
+ id: heading[1]!,
90
+ title: heading[2]!.trim(),
91
+ href,
92
+ date: metadata("Data"),
93
+ status,
94
+ summary:
95
+ context
96
+ ?.replace(/^#{1,6}[\t ].*(?:\r?\n|$)/gm, "")
97
+ .trim()
98
+ .split(/\n\s*\n/)[0]
99
+ ?.trim() || undefined,
100
+ searchText: source,
101
+ body: source,
102
+ };
103
+ }
104
+
105
+ export function releaseRecords(
106
+ source: string,
107
+ basePath: string,
108
+ ): DocumentationRecord[] {
109
+ return parseChangelog(source).versions.map((release) => {
110
+ return {
111
+ id: release.version,
112
+ title: release.version,
113
+ href: `${basePath}/${encodeURIComponent(release.version)}`,
114
+ date: release.date,
115
+ content: release.beforeBreaking ?? release.body,
116
+ breaking: release.breaking,
117
+ afterBreaking: release.afterBreaking,
118
+ body: `# ${release.version}\n\n${release.date ?? ""}\n\n${release.sourceBody}`,
119
+ searchText: `${release.version} ${release.date ?? ""} ${release.sourceBody}`,
120
+ };
121
+ });
122
+ }
123
+
124
+ function searchable(value: string): string {
125
+ return value
126
+ .normalize("NFD")
127
+ .replace(/[\u0300-\u036f]/g, "")
128
+ .toLocaleLowerCase("pt-BR");
129
+ }
130
+
131
+ export function filterDocumentationRecords(
132
+ records: readonly DocumentationRecord[],
133
+ query: string,
134
+ ): DocumentationRecord[] {
135
+ const terms = searchable(query).trim().split(/\s+/).filter(Boolean);
136
+ return records.filter(
137
+ (record) =>
138
+ terms.every((term) =>
139
+ searchable(
140
+ `${record.id} ${record.title} ${record.status ?? ""} ${record.searchText}`,
141
+ ).includes(term),
142
+ ),
143
+ );
144
+ }