@softize/opus 10.0.0 → 11.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 +43 -0
- package/bin/cli.mjs +1 -1
- package/bin/lib/db-check-runner.mjs +5 -5
- package/bin/lib/db-migrate-runner.mjs +3 -3
- package/bin/lib/db-scaffold-runner.mjs +4 -4
- package/bin/lib/db.mjs +1 -1
- package/bin/lib/gen-manifest.mjs +0 -3
- package/bin/lib/gen-runner.mjs +2 -7
- package/bin/lib/init.mjs +6 -0
- package/bin/lib/materialize.mjs +3 -1
- package/docs/data-layer.md +2 -2
- package/docs/protocol.md +2 -2
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -0
- package/registry/skills/build-opus-ui/SKILL.md +2 -0
- package/registry/skills/create-opus-action/SKILL.md +2 -0
- package/registry/skills/implement-opus-change/SKILL.md +2 -0
- package/registry/skills/upgrade-opus/SKILL.md +2 -0
- package/registry/skills/write-product-communication/SKILL.md +28 -0
- package/registry/skills/write-product-communication/agents/openai.yaml +4 -0
- package/src/core/domain.ts +3 -6
- package/src/data/readonly-pool.ts +2 -2
- package/src/schema/entity.ts +1 -1
- package/src/ui/components/patterns/shell-nav.tsx +5 -9
- package/src/ui/components/patterns/sidebar.tsx +40 -15
- package/src/ui/docs/DocBrowser.tsx +25 -10
- package/src/ui/docs/content/actions.md +7 -6
- package/src/ui/docs/content/auth.md +3 -2
- package/src/ui/docs/content/cli.md +2 -1
- package/src/ui/docs/content/confirm.md +2 -2
- package/src/ui/docs/content/data.md +1 -1
- package/src/ui/docs/content/getting-started.md +15 -23
- package/src/ui/docs/content/microcopy.md +119 -72
- package/src/ui/docs/content/sidebar.md +21 -1
- package/src/ui/docs/content/upgrading.md +1 -1
- package/src/ui/docs/registry.tsx +1 -7
- package/src/ui/meta.ts +2 -23
- package/src/ui/react.tsx +4 -19
- package/docs/shellnav.md +0 -131
- package/src/ui/components/patterns/app-shell.tsx +0 -227
- package/src/ui/components/patterns/section-shell.tsx +0 -246
- package/src/ui/docs/content/app-shell.md +0 -155
- package/src/ui/docs/content/resizable.md +0 -86
- package/src/ui/docs/content/section-shell.md +0 -121
package/CHANGELOG.md
CHANGED
|
@@ -6,11 +6,54 @@ Este arquivo viaja no pacote: num projeto, leia `node_modules/@softize/opus/CHAN
|
|
|
6
6
|
Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
7
7
|
`manifest:check`) — eles apontam o que a mudança cobra do seu código.
|
|
8
8
|
|
|
9
|
+
## 11.1.0 — 2026-08-21
|
|
10
|
+
|
|
11
|
+
O Opus passa a distribuir a skill `write-product-communication` para orientar documentação,
|
|
12
|
+
interface, mensagens de erro, CLI, onboarding e textos explicativos a partir da situação do
|
|
13
|
+
leitor. A página antes dedicada apenas a microcopy agora reúne a referência canônica de
|
|
14
|
+
comunicação, incluindo escrita normativa e tratamento de erros.
|
|
15
|
+
|
|
16
|
+
O `opus setup` expande o conteúdo dessa referência ao materializar a skill em `.agents` e
|
|
17
|
+
`.claude`. As instruções permanentes e as skills de UI, actions, mudanças e upgrades passam a
|
|
18
|
+
acionar a orientação nos fluxos em que produzem texto. Testes de criação e setup confirmam que
|
|
19
|
+
projetos novos recebem o conteúdo completo, sem depender de um link externo.
|
|
20
|
+
|
|
21
|
+
A home, as páginas iniciais e a mensagem de sucesso do `opus check` começam a migração da voz
|
|
22
|
+
existente: apresentam contexto e efeito antes das regras, evitam slogans e informam a próxima
|
|
23
|
+
etapa em linguagem natural.
|
|
24
|
+
|
|
9
25
|
> As entradas 2.30.1–2.31.5 foram **reconstruídas do git** (o release publicava sem
|
|
10
26
|
> passar por aqui). Uma delas — a 2.31.5, que mudou o rem base — é visual GLOBAL e
|
|
11
27
|
> tinha ficado sem registro nenhum, o que é exatamente o caso que este arquivo existe
|
|
12
28
|
> pra cobrir.
|
|
13
29
|
|
|
30
|
+
## 11.0.0 — 2026-08-21
|
|
31
|
+
|
|
32
|
+
**Breaking — deprecateds removidos na fronteira pública.** O major anterior manteve por
|
|
33
|
+
engano APIs cuja própria documentação prometia remoção na próxima versão; esta versão
|
|
34
|
+
fecha a migração em vez de carregar duas taxonomias indefinidamente.
|
|
35
|
+
|
|
36
|
+
- `AppShell`, `AppShellBar`, `AppShellTrigger`, `useAppShell`, `useSidebarSlot` e
|
|
37
|
+
`SectionShell` (incluindo seus tipos `Section*`) saem. Componha chrome e seções com
|
|
38
|
+
`Split`, `Pane`, `Sidebar`, `PaneHeader`, `PaneContent`, `PaneFooter` e `SidebarNav`.
|
|
39
|
+
- `ResizablePanelGroup`, `ResizablePanel` e `ResizableHandle` deixam de ser exports
|
|
40
|
+
públicos. Use `<Split resizable>` e declare tamanho/limites nos `<Pane>`.
|
|
41
|
+
- `SidebarHeader`, `SidebarContent` e `SidebarFooter` saem; eram aliases de
|
|
42
|
+
`PaneHeader`, `PaneContent` e `PaneFooter`.
|
|
43
|
+
- `DomainConfig.models` sai. Declare entidades em `DomainConfig.entities`; os comandos
|
|
44
|
+
`opus db` e o gerador agora leem somente esse campo. O manifest deixa de emitir o array
|
|
45
|
+
redundante `models`; os nomes continuam disponíveis em `entities[].name`.
|
|
46
|
+
|
|
47
|
+
`SidebarNav` passa a aceitar `subgroups`, cobrindo navegação contextual e a árvore de três
|
|
48
|
+
níveis da documentação sem um shell especializado. `ShellNav` agora deriva o estado
|
|
49
|
+
recolhido diretamente da `Sidebar`. O próprio `DocBrowser` foi migrado para a composição
|
|
50
|
+
canônica e serve como consumidor de referência.
|
|
51
|
+
|
|
52
|
+
**Migração:** substitua shells prontos pela composição `Split > Pane > Sidebar`; troque os
|
|
53
|
+
aliases `Sidebar*` pelos slots `Pane*`; envolva painéis ajustáveis em `<Split resizable>`;
|
|
54
|
+
renomeie `domain.models` para `domain.entities` e, ao consumir manifest, derive nomes de
|
|
55
|
+
`domain.entities.map(entity => entity.name)`.
|
|
56
|
+
|
|
14
57
|
## 10.0.0 — 2026-08-20
|
|
15
58
|
|
|
16
59
|
**Breaking — forma usa a escala do Tailwind e superfícies sempre carregam seu foreground.**
|
package/bin/cli.mjs
CHANGED
|
@@ -190,7 +190,7 @@ async function cmdCheck(dir) {
|
|
|
190
190
|
}
|
|
191
191
|
const viaContrato = contracts > 0 ? ` (${contracts} contrato(s))` : ''
|
|
192
192
|
if (findings.length === 0) {
|
|
193
|
-
log('success', `✓ opus check: ${actions} action(s)${viaContrato}
|
|
193
|
+
log('success', `✓ opus check: ${actions} action(s)${viaContrato}; nenhuma violação encontrada.`)
|
|
194
194
|
return
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Contrato do `opus.config.ts` pros comandos `db`:
|
|
10
10
|
* - `entities?: EntityConfig[]` — entidades explícitas (opcional)
|
|
11
|
-
* - `domains?: DomainConfig[]` — entidades coletadas de `domain.
|
|
11
|
+
* - `domains?: DomainConfig[]` — entidades coletadas de `domain.entities`
|
|
12
12
|
* - `database: () => Kysely | Promise<Kysely>` — factory LAZY do banco.
|
|
13
13
|
* É factory (não instância) de propósito: o `gen` importa o mesmo config
|
|
14
14
|
* sem nunca chamar `database()`, então não abre conexão.
|
|
@@ -29,9 +29,9 @@ function emit(obj) {
|
|
|
29
29
|
|
|
30
30
|
function collectEntities(domain, out) {
|
|
31
31
|
if (domain === null || typeof domain !== 'object') return
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
for (const value of Object.values(
|
|
32
|
+
const entities = domain.entities
|
|
33
|
+
if (entities !== undefined && entities !== null && typeof entities === 'object') {
|
|
34
|
+
for (const value of Object.values(entities)) {
|
|
35
35
|
if (isEntityConfig(value)) out.push(value)
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -59,7 +59,7 @@ async function main() {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
if (entities.length === 0) {
|
|
62
|
-
emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.
|
|
62
|
+
emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.entities)' })
|
|
63
63
|
return
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -28,9 +28,9 @@ function emit(obj) {
|
|
|
28
28
|
|
|
29
29
|
function collectEntities(domain, out) {
|
|
30
30
|
if (domain === null || typeof domain !== 'object') return
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
for (const value of Object.values(
|
|
31
|
+
const entities = domain.entities
|
|
32
|
+
if (entities !== undefined && entities !== null && typeof entities === 'object') {
|
|
33
|
+
for (const value of Object.values(entities)) if (isEntityConfig(value)) out.push(value)
|
|
34
34
|
}
|
|
35
35
|
if (Array.isArray(domain.subdomains)) for (const sub of domain.subdomains) collectEntities(sub, out)
|
|
36
36
|
}
|
|
@@ -23,9 +23,9 @@ function emit(obj) {
|
|
|
23
23
|
|
|
24
24
|
function collectEntities(domain, out) {
|
|
25
25
|
if (domain === null || typeof domain !== 'object') return
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
for (const value of Object.values(
|
|
26
|
+
const entities = domain.entities
|
|
27
|
+
if (entities !== undefined && entities !== null && typeof entities === 'object') {
|
|
28
|
+
for (const value of Object.values(entities)) if (isEntityConfig(value)) out.push(value)
|
|
29
29
|
}
|
|
30
30
|
if (Array.isArray(domain.subdomains)) for (const sub of domain.subdomains) collectEntities(sub, out)
|
|
31
31
|
}
|
|
@@ -48,7 +48,7 @@ async function main() {
|
|
|
48
48
|
if (Array.isArray(config.entities)) for (const e of config.entities) if (isEntityConfig(e)) entities.push(e)
|
|
49
49
|
if (Array.isArray(config.domains)) for (const d of config.domains) collectEntities(d, entities)
|
|
50
50
|
if (entities.length === 0) {
|
|
51
|
-
emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.
|
|
51
|
+
emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.entities)' })
|
|
52
52
|
return
|
|
53
53
|
}
|
|
54
54
|
if (typeof config.database !== 'function') {
|
package/bin/lib/db.mjs
CHANGED
|
@@ -249,7 +249,7 @@ Flags:
|
|
|
249
249
|
|
|
250
250
|
O opus.config.ts precisa expor, pros comandos db:
|
|
251
251
|
database: () => Kysely factory LAZY do banco (gen não a chama)
|
|
252
|
-
entities | domains entidades (explícitas ou via domain.
|
|
252
|
+
entities | domains entidades (explícitas ou via domain.entities)
|
|
253
253
|
schema?: string script SQL idempotente (default: ./db/schema.sql)
|
|
254
254
|
migrations?: string pasta dos rascunhos do scaffold (default: ./migrations)
|
|
255
255
|
|
package/bin/lib/gen-manifest.mjs
CHANGED
|
@@ -25,10 +25,7 @@ function shapeDomain(d) {
|
|
|
25
25
|
dicts: d.dicts,
|
|
26
26
|
repository: d.hasRepository,
|
|
27
27
|
service: d.hasService,
|
|
28
|
-
// `entities` = a fonte estrutural+negócio (campos + docs). `models` = nomes
|
|
29
|
-
// (legado, removido no PR de rename). Ambos por ora pra não quebrar a lente.
|
|
30
28
|
entities: d.entities ?? [],
|
|
31
|
-
models: d.modelNames,
|
|
32
29
|
actions: d.actions.map(shapeAction),
|
|
33
30
|
reactions: d.reactions,
|
|
34
31
|
schedules: d.schedules,
|
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* SerializedDomain:
|
|
31
31
|
* {
|
|
32
32
|
* name, dicts, actions, reactions, schedules, subdomains,
|
|
33
|
-
* hasRepository, hasService
|
|
33
|
+
* hasRepository, hasService
|
|
34
34
|
* }
|
|
35
35
|
*
|
|
36
36
|
* SerializedAction (todos os ActionDef preservam suas strings):
|
|
@@ -143,17 +143,12 @@ async function main() {
|
|
|
143
143
|
// =============================================================================
|
|
144
144
|
|
|
145
145
|
function serializeDomain(domain, ctx) {
|
|
146
|
-
|
|
147
|
-
const entitySrc = domain.entities ?? domain.models
|
|
148
|
-
const entityList = serializeEntities(entitySrc)
|
|
146
|
+
const entityList = serializeEntities(domain.entities)
|
|
149
147
|
return {
|
|
150
148
|
name: domain.name,
|
|
151
149
|
description: nullable(domain.description),
|
|
152
150
|
hasRepository: domain.repository !== undefined,
|
|
153
151
|
hasService: domain.service !== undefined,
|
|
154
|
-
// Compat: nomes ainda expostos; `entities` traz a estrutura+doc completa.
|
|
155
|
-
hasModels: entityList.length > 0,
|
|
156
|
-
modelNames: entityList.map((e) => e.name),
|
|
157
152
|
entities: entityList,
|
|
158
153
|
dicts: serializeDicts(domain.dicts),
|
|
159
154
|
actions: Array.from(iterateActions(domain.actions), (a) =>
|
package/bin/lib/init.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** opus setup: aplica o marcador per-app e os artefatos Opus rastreáveis no repo. */
|
|
2
2
|
|
|
3
3
|
import { promises as fs } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
|
|
6
7
|
import { materializeOpus, PACKAGE_VERSION } from "./materialize.mjs";
|
|
@@ -27,7 +28,12 @@ async function readJson(p) {
|
|
|
27
28
|
/** Raiz do repo (sobe até achar `.git`); sem repo → null (o chamador decide o fallback). */
|
|
28
29
|
export async function repoRootOf(dir) {
|
|
29
30
|
let cur = path.resolve(dir);
|
|
31
|
+
const temporaryRoot = path.resolve(tmpdir());
|
|
30
32
|
for (;;) {
|
|
33
|
+
// O diretório temporário global pode receber marcadores transitórios de ferramentas
|
|
34
|
+
// e sandboxes. Um projeto dentro dele ainda pode ter sua própria raiz Git, mas não deve
|
|
35
|
+
// herdar `/tmp/.git` como se todos os fixtures pertencessem ao mesmo repositório.
|
|
36
|
+
if (cur === temporaryRoot) return null;
|
|
31
37
|
if (await exists(path.join(cur, ".git"))) return cur;
|
|
32
38
|
const up = path.dirname(cur);
|
|
33
39
|
if (up === cur) return null;
|
package/bin/lib/materialize.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, extname, join, relative, resolve } from 'node:path'
|
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
7
|
import { validateSkill } from './validate-skill.mjs'
|
|
8
|
+
import { expandDocIncludes } from './docs-include.mjs'
|
|
8
9
|
|
|
9
10
|
export const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
10
11
|
export const PACKAGE_NAME = '@softize/opus'
|
|
@@ -97,7 +98,8 @@ function expectedFiles(exclude) {
|
|
|
97
98
|
for (const file of filesUnder(join(PACKAGE_ROOT, sourceRoot))) {
|
|
98
99
|
const source = `${sourceRoot}/${file.relative}`
|
|
99
100
|
const destination = `${destinationRoot}/${file.relative}`
|
|
100
|
-
const
|
|
101
|
+
const sourceContent = readFileSync(file.path, 'utf8')
|
|
102
|
+
const content = file.relative === 'SKILL.md' ? expandDocIncludes(sourceContent) : sourceContent
|
|
101
103
|
expected.set(destination, { source, content, output: addMarker(destination, source, content) })
|
|
102
104
|
}
|
|
103
105
|
}
|
package/docs/data-layer.md
CHANGED
|
@@ -90,7 +90,7 @@ infra do `gen`, não a do `check`). Contrato do config **pros comandos `db`**:
|
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
92
|
export default {
|
|
93
|
-
domains: [...], // entidades coletadas de domain.
|
|
93
|
+
domains: [...], // entidades coletadas de domain.entities
|
|
94
94
|
entities: [Deal, Company], // ou explícitas (opcional)
|
|
95
95
|
schema: './src/db/schema.sql', // o script idempotente (default: ./db/schema.sql)
|
|
96
96
|
database: () => new Kysely({ ... }), // factory LAZY do banco
|
|
@@ -139,7 +139,7 @@ import { Kysely, PostgresDialect, CamelCasePlugin } from 'kysely'
|
|
|
139
139
|
import { Deal } from './src/domains/deals/deal.entity.ts'
|
|
140
140
|
|
|
141
141
|
export default {
|
|
142
|
-
entities: [Deal], // ou domains: [...] (entidades via domain.
|
|
142
|
+
entities: [Deal], // ou domains: [...] (entidades via domain.entities)
|
|
143
143
|
naming: 'snake', // colunas snake no banco
|
|
144
144
|
migrations: './migrations',
|
|
145
145
|
database: () => new Kysely({
|
package/docs/protocol.md
CHANGED
|
@@ -2053,14 +2053,14 @@ defineAction({
|
|
|
2053
2053
|
|
|
2054
2054
|
### Entidades dentro de um domínio
|
|
2055
2055
|
|
|
2056
|
-
`DomainConfig.
|
|
2056
|
+
`DomainConfig.entities` recebe as entidades do recorte. O domínio agrupa; a entidade declara.
|
|
2057
2057
|
|
|
2058
2058
|
```ts
|
|
2059
2059
|
import { defineDomain } from '@softize/opus'
|
|
2060
2060
|
|
|
2061
2061
|
export const crm = defineDomain({
|
|
2062
2062
|
name: 'crm',
|
|
2063
|
-
|
|
2063
|
+
entities: { Deal, Company, Contact },
|
|
2064
2064
|
actions: { /* deal.create, deal.search, ... */ },
|
|
2065
2065
|
})
|
|
2066
2066
|
```
|
package/package.json
CHANGED
|
@@ -15,3 +15,6 @@ artefatos de agentes fica em `base.json`; cada app Opus mantém seu marcador `op
|
|
|
15
15
|
branch antes de commit/review e usar `upgrade-opus` para ler changelog e aplicar migrações.
|
|
16
16
|
- Consultar as skills Opus materializadas conforme o workflow; não atribuir ao SDK decisões
|
|
17
17
|
universais de domínio ou arquitetura.
|
|
18
|
+
- Usar `write-product-communication` ao criar ou alterar documentação, textos de interface,
|
|
19
|
+
mensagens de erro, CLI, onboarding ou explicações. Partir da situação do leitor, apresentar
|
|
20
|
+
contexto antes da regra e evitar slogans, absolutos e linguagem de manifesto.
|
|
@@ -26,6 +26,8 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
26
26
|
7. Usar diretamente a escala `rounded-*`; não criar radius por nome de componente quando
|
|
27
27
|
`rounded-sm` a `rounded-xl` já expressam a forma.
|
|
28
28
|
8. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
|
|
29
|
+
9. Usar `write-product-communication` para labels, ajuda, estados vazios, confirmações e erros;
|
|
30
|
+
revisar o fluxo completo, não apenas cada string isolada.
|
|
29
31
|
|
|
30
32
|
## Verificação
|
|
31
33
|
|
|
@@ -21,6 +21,8 @@ registrados no runtime do projeto.
|
|
|
21
21
|
1. Rodar `node scripts/scaffold.mjs <resource> <verb> <kind>` a partir desta skill.
|
|
22
22
|
2. Colocar o contrato em módulo importável pelos consumidores e o binding no lado servidor.
|
|
23
23
|
3. Substituir os exemplos do scaffold por schemas, descrições e comportamento do domínio.
|
|
24
|
+
Usar `write-product-communication` nas descrições, mensagens e textos projetados para UI
|
|
25
|
+
ou documentação.
|
|
24
26
|
4. Declarar `requires` somente com `authorize` efetivo; `requires` sozinho não protege a action.
|
|
25
27
|
5. Exportar e registrar o binding no domínio/runtime existente.
|
|
26
28
|
6. Acionar `test-opus-action` para cobrir contrato, sucesso, erros e efeitos relevantes.
|
|
@@ -25,6 +25,8 @@ dependências server-only, bindings registrados, testes e gates verdes.
|
|
|
25
25
|
5. Usar `create-opus-action`, `test-opus-action` ou `build-opus-ui` quando a mudança entrar
|
|
26
26
|
nesses workflows especializados.
|
|
27
27
|
6. Atualizar manifest, docs geradas e exemplos somente pelos comandos do repo.
|
|
28
|
+
7. Usar `write-product-communication` sempre que a mudança alcançar documentação, UI, CLI,
|
|
29
|
+
mensagens de erro ou outro texto destinado a uma pessoa.
|
|
28
30
|
|
|
29
31
|
## Verificação
|
|
30
32
|
|
|
@@ -19,6 +19,8 @@ materializados e projeções geradas consistentes no mesmo diff.
|
|
|
19
19
|
4. Aplicar migrações no código por domínio, sem compatibilidade temporária silenciosa.
|
|
20
20
|
5. Rodar `opus check`, typecheck, testes, build e geradores declarados pelo projeto.
|
|
21
21
|
6. Conferir `base.json`, lockfile, skills, hooks, instruções e outputs gerados no diff.
|
|
22
|
+
7. Usar `write-product-communication` ao redigir changelog, guia de migração e mensagens
|
|
23
|
+
adicionadas ou alteradas durante o upgrade.
|
|
22
24
|
|
|
23
25
|
## Verificação
|
|
24
26
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-product-communication
|
|
3
|
+
description: Escreve e revisa comunicação de produto clara, humana e orientada ao leitor. Usar ao criar ou alterar documentação, mensagens de erro, textos de interface, CLI, onboarding, ajuda, explicações, comentários ou qualquer conteúdo que uma pessoa precise compreender.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Escrever comunicação de produto
|
|
7
|
+
|
|
8
|
+
## Resultado
|
|
9
|
+
|
|
10
|
+
Entregar uma comunicação que parte da situação do leitor, oferece o contexto necessário e
|
|
11
|
+
permite que a pessoa compreenda ou prossiga sem decodificar a linguagem interna da equipe.
|
|
12
|
+
|
|
13
|
+
## Procedimento
|
|
14
|
+
|
|
15
|
+
1. Identificar quem lerá o texto, o que essa pessoa tenta fazer e o que precisa compreender ou
|
|
16
|
+
decidir em seguida.
|
|
17
|
+
2. Ler a comunicação no fluxo completo em que aparecerá, incluindo estados anteriores e
|
|
18
|
+
posteriores.
|
|
19
|
+
3. Escrever a partir do problema e do efeito percebido; introduzir mecanismos e termos técnicos
|
|
20
|
+
somente quando acrescentarem precisão.
|
|
21
|
+
4. Distinguir princípios, regras, recomendações, comportamentos, verificações e exceções.
|
|
22
|
+
5. Revisar o texto fora do contexto da implementação e remover pressupostos, slogans,
|
|
23
|
+
advertências desnecessárias e detalhes internos.
|
|
24
|
+
6. Confirmar que erros explicam o ocorrido, o impacto e uma próxima ação real quando ela existir.
|
|
25
|
+
|
|
26
|
+
## Referência canônica
|
|
27
|
+
|
|
28
|
+
<!-- opus-doc: microcopy.md -->
|
package/src/core/domain.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* tbdlib — `defineDomain` factory + flatten helper.
|
|
3
3
|
*
|
|
4
4
|
* Domain agrupa as peças que pertencem a um mesmo recorte funcional
|
|
5
|
-
* (dicts,
|
|
5
|
+
* (dicts, entities, repository, service, actions, reactions, schedules,
|
|
6
6
|
* subdomains). É puramente declarativo: nenhum efeito colateral acontece
|
|
7
7
|
* em `defineDomain`. O runtime consome o objeto via `flattenDomain` quando
|
|
8
8
|
* recebe um `DomainConfig` em `register(...)`.
|
|
@@ -55,12 +55,9 @@ export interface DomainConfig {
|
|
|
55
55
|
dicts?: Record<string, unknown>
|
|
56
56
|
|
|
57
57
|
/** Entidades do domínio (`defineEntity`). Map de nome → EntityConfig. É a fonte
|
|
58
|
-
* estrutural+negócio que vai pro manifest.
|
|
58
|
+
* estrutural+negócio que vai pro manifest. */
|
|
59
59
|
entities?: Record<string, unknown>
|
|
60
60
|
|
|
61
|
-
/** @deprecated Use `entities`. Mantido por compat — o runtime/gen lê os dois. */
|
|
62
|
-
models?: Record<string, unknown>
|
|
63
|
-
|
|
64
61
|
/** Repository class do domínio. */
|
|
65
62
|
repository?: unknown
|
|
66
63
|
|
|
@@ -152,7 +149,7 @@ export function isDomainConfig(value: unknown): value is DomainConfig {
|
|
|
152
149
|
'schedules' in v ||
|
|
153
150
|
'subdomains' in v ||
|
|
154
151
|
'dicts' in v ||
|
|
155
|
-
'
|
|
152
|
+
'entities' in v ||
|
|
156
153
|
'repository' in v ||
|
|
157
154
|
'service' in v
|
|
158
155
|
)
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 1. **Pool com teto** — crie o `Pool` com `max` (e/ou use `maxConcurrent` aqui):
|
|
7
7
|
* query de LLM não esgota as conexões do app.
|
|
8
|
-
* 2. **Transação READ ONLY** — `BEGIN TRANSACTION READ ONLY`:
|
|
9
|
-
*
|
|
8
|
+
* 2. **Transação READ ONLY** — `BEGIN TRANSACTION READ ONLY`: o servidor rejeita
|
|
9
|
+
* qualquer tentativa de escrita, independentemente do SQL recebido.
|
|
10
10
|
* 3. **`SET LOCAL ROLE` por transação** — o alcance é do contexto: com grants
|
|
11
11
|
* default-fechado, `sales_read` não enxerga `hr_employees.salary` nem por
|
|
12
12
|
* SELECT criativo. (Criar os roles/grants é migração sua; aqui só se assume.)
|
package/src/schema/entity.ts
CHANGED
|
@@ -310,7 +310,7 @@ export function entityTable(config: EntityConfig): string {
|
|
|
310
310
|
|
|
311
311
|
/**
|
|
312
312
|
* `true` se `value` parece um `EntityConfig`. Usado pra coletar entidades de
|
|
313
|
-
* `domain.
|
|
313
|
+
* `domain.entities` (que é `unknown`) sem confundir com action/reaction/model.
|
|
314
314
|
* Heurística: tem `name` string + `fields` objeto e **não** tem `kind`
|
|
315
315
|
* (descarta `ActionDef`).
|
|
316
316
|
*/
|
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
* <ShellNav /> — o menu como primitivo PIVOTÁVEL. Um nav (grupos → itens, com heading e
|
|
3
3
|
* âncora inferior) que serve os DOIS lares do chrome sem flag:
|
|
4
4
|
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
* slot do AppShell (`useSidebarSlot`) — o app não passa isso em duas mãos.
|
|
8
|
-
* - dentro do conteúdo (o que o <SectionShell> faz) → sempre expandido (fora da sidebar,
|
|
9
|
-
* `useSidebarSlot` é null).
|
|
5
|
+
* - dentro de <Sidebar> → recolhe pra ícone-só (com tooltip) junto com a coluna;
|
|
6
|
+
* - fora de <Sidebar> → permanece expandido.
|
|
10
7
|
*
|
|
11
8
|
* "Tanto faz onde": o componente se adapta ao lugar, sem "modo" configurado.
|
|
12
9
|
*
|
|
@@ -17,7 +14,7 @@
|
|
|
17
14
|
|
|
18
15
|
import type { ReactNode } from 'react'
|
|
19
16
|
import { cn } from '../../lib/cn.ts'
|
|
20
|
-
import {
|
|
17
|
+
import { useSidebarCollapsed } from './sidebar.tsx'
|
|
21
18
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip.tsx'
|
|
22
19
|
|
|
23
20
|
export interface ShellNavItem {
|
|
@@ -49,7 +46,7 @@ export interface ShellNavProps {
|
|
|
49
46
|
footer?: ShellNavGroup[]
|
|
50
47
|
/** Rótulo da landmark `<nav>`. */
|
|
51
48
|
navLabel?: string
|
|
52
|
-
/** Classes da raiz (a coluna). A
|
|
49
|
+
/** Classes da raiz (a coluna). A largura vem do pane/sidebar que a contém. */
|
|
53
50
|
className?: string
|
|
54
51
|
}
|
|
55
52
|
|
|
@@ -109,8 +106,7 @@ function renderGroup(group: ShellNavGroup, gi: number, activeId: string | undefi
|
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
export function ShellNav({ groups, activeId, onSelect, heading, footer, navLabel = 'Navegação', className }: ShellNavProps): React.ReactElement {
|
|
112
|
-
|
|
113
|
-
const railed = useSidebarSlot()?.collapsed ?? false
|
|
109
|
+
const railed = useSidebarCollapsed()
|
|
114
110
|
|
|
115
111
|
const body = (
|
|
116
112
|
<div data-slot="shell-nav" className={cn('flex h-full min-h-0 flex-col', className)}>
|
|
@@ -5,6 +5,11 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../pri
|
|
|
5
5
|
interface SidebarContextValue { collapsed: boolean }
|
|
6
6
|
const SidebarContext = createContext<SidebarContextValue | null>(null)
|
|
7
7
|
|
|
8
|
+
/** Estado da Sidebar mais próxima. Uso interno por navegações que se adaptam ao colapso. */
|
|
9
|
+
export function useSidebarCollapsed(): boolean {
|
|
10
|
+
return useContext(SidebarContext)?.collapsed ?? false
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export interface SidebarProps {
|
|
9
14
|
collapsed?: boolean
|
|
10
15
|
/** Desliga a borda quando o Split já desenha a divisória. */
|
|
@@ -39,27 +44,21 @@ export function PaneFooter({ className, children }: { className?: string; childr
|
|
|
39
44
|
return <div data-slot="pane-footer" className={cn('shrink-0 border-t border-border p-2', className)}>{children}</div>
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
/** @deprecated Use PaneHeader. */
|
|
43
|
-
export const SidebarHeader = PaneHeader
|
|
44
|
-
/** @deprecated Use PaneContent. */
|
|
45
|
-
export const SidebarContent = PaneContent
|
|
46
|
-
/** @deprecated Use PaneFooter. */
|
|
47
|
-
export const SidebarFooter = PaneFooter
|
|
48
|
-
|
|
49
47
|
export interface SidebarItemProps {
|
|
50
48
|
label: string
|
|
51
49
|
icon?: ReactNode
|
|
52
50
|
badge?: ReactNode
|
|
53
51
|
active?: boolean
|
|
54
52
|
disabled?: boolean
|
|
53
|
+
className?: string
|
|
55
54
|
onClick: () => void
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
/** Item de navegação que se adapta ao colapso da Sidebar em que está. */
|
|
59
|
-
export function SidebarItem({ label, icon, badge, active = false, disabled = false, onClick }: SidebarItemProps): React.ReactElement {
|
|
60
|
-
const collapsed =
|
|
58
|
+
export function SidebarItem({ label, icon, badge, active = false, disabled = false, className, onClick }: SidebarItemProps): React.ReactElement {
|
|
59
|
+
const collapsed = useSidebarCollapsed()
|
|
61
60
|
const button = (
|
|
62
|
-
<button type="button" disabled={disabled} aria-current={active ? 'page' : undefined} onClick={onClick} className={cn('flex items-center gap-2.5 rounded-md transition-colors', 'disabled:pointer-events-none disabled:opacity-40', collapsed ? 'mx-auto size-9 justify-center p-0' : 'w-full px-2.5 py-1.5 text-left text-sm', active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60')}>
|
|
61
|
+
<button type="button" disabled={disabled} aria-current={active ? 'page' : undefined} onClick={onClick} className={cn('flex items-center gap-2.5 rounded-md transition-colors', 'disabled:pointer-events-none disabled:opacity-40', collapsed ? 'mx-auto size-9 justify-center p-0' : 'w-full px-2.5 py-1.5 text-left text-sm', active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60', className)}>
|
|
63
62
|
{icon !== undefined && <span className="flex shrink-0">{icon}</span>}
|
|
64
63
|
{!collapsed && <span className="min-w-0 flex-1 truncate">{label}</span>}
|
|
65
64
|
{!collapsed && badge !== undefined && <span className="shrink-0">{badge}</span>}
|
|
@@ -71,17 +70,43 @@ export function SidebarItem({ label, icon, badge, active = false, disabled = fal
|
|
|
71
70
|
|
|
72
71
|
export interface SidebarNavGroup {
|
|
73
72
|
label?: string
|
|
74
|
-
items
|
|
73
|
+
items?: SidebarNavItem[]
|
|
74
|
+
subgroups?: SidebarNavSubgroup[]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type SidebarNavItem = Omit<SidebarItemProps, 'active' | 'onClick'> & { id: string }
|
|
78
|
+
|
|
79
|
+
export interface SidebarNavSubgroup {
|
|
80
|
+
label?: string
|
|
81
|
+
items: SidebarNavItem[]
|
|
75
82
|
}
|
|
76
83
|
|
|
84
|
+
const titled = (label?: string): boolean => (label ?? '').trim() !== ''
|
|
85
|
+
const subFilled = (subgroup: SidebarNavSubgroup): boolean => subgroup.items.length > 0
|
|
86
|
+
const groupFilled = (group: SidebarNavGroup): boolean =>
|
|
87
|
+
(group.items?.length ?? 0) > 0 || (group.subgroups?.some(subFilled) ?? false)
|
|
88
|
+
|
|
77
89
|
/** Navegação controlada para Sidebar. Header/footer continuam slots explícitos do pai. */
|
|
78
90
|
export function SidebarNav({ groups, activeId, onSelect, navLabel = 'Navegação', className }: { groups: SidebarNavGroup[]; activeId?: string; onSelect: (id: string) => void; navLabel?: string; className?: string }): React.ReactElement {
|
|
79
|
-
const collapsed =
|
|
91
|
+
const collapsed = useSidebarCollapsed()
|
|
92
|
+
const renderItem = (item: SidebarNavItem, nested = false): React.ReactElement => (
|
|
93
|
+
<SidebarItem
|
|
94
|
+
key={item.id}
|
|
95
|
+
{...item}
|
|
96
|
+
className={cn(nested && !collapsed && 'pl-6', item.className)}
|
|
97
|
+
active={item.id === activeId}
|
|
98
|
+
onClick={() => onSelect(item.id)}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
80
101
|
const body = (
|
|
81
102
|
<nav aria-label={navLabel} data-slot="sidebar-nav" className={cn('space-y-4 p-2', className)}>
|
|
82
|
-
{groups.map((group, gi) => <div key={gi} className="space-y-0.5">
|
|
83
|
-
{!collapsed && group.label
|
|
84
|
-
{group.items
|
|
103
|
+
{groups.map((group, gi) => !groupFilled(group) ? null : <div key={gi} className="space-y-0.5">
|
|
104
|
+
{!collapsed && titled(group.label) && <div className="px-2.5 pb-1 text-xs font-medium text-muted-foreground/70">{group.label}</div>}
|
|
105
|
+
{group.items?.map((item) => renderItem(item))}
|
|
106
|
+
{group.subgroups?.map((subgroup, si) => !subFilled(subgroup) ? null : <div key={si} className="space-y-0.5">
|
|
107
|
+
{!collapsed && titled(subgroup.label) && <div className="pb-0.5 pl-4 pr-2.5 pt-1.5 text-[11px] font-medium text-muted-foreground/50">{subgroup.label}</div>}
|
|
108
|
+
{subgroup.items.map((item) => renderItem(item, titled(subgroup.label)))}
|
|
109
|
+
</div>)}
|
|
85
110
|
</div>)}
|
|
86
111
|
</nav>
|
|
87
112
|
)
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { useEffect } from 'react'
|
|
16
16
|
import { DOC_SECTIONS, type DocSection, type DocEntry } from './registry'
|
|
17
|
-
import {
|
|
17
|
+
import { Pane, Split } from '../components/patterns/split.tsx'
|
|
18
|
+
import { PaneContent, Sidebar, SidebarNav, type SidebarNavGroup } from '../components/patterns/sidebar.tsx'
|
|
18
19
|
import { navigate, usePathname } from '../router.ts'
|
|
19
20
|
|
|
20
21
|
function findPage(sections: DocSection[], slug: string): DocEntry | undefined {
|
|
@@ -70,21 +71,35 @@ export function DocBrowser({
|
|
|
70
71
|
// Seção → grupo, e os grupos da seção → subgrupos. O tier do meio NÃO pode ser
|
|
71
72
|
// achatado: `docSectionsFromFolder` o preenche a partir de sub-pasta (ou do
|
|
72
73
|
// frontmatter `group:`), que é o caminho do `opusDocs({ source })`.
|
|
73
|
-
const navGroups:
|
|
74
|
+
const navGroups: SidebarNavGroup[] = sections.map((section) => ({
|
|
74
75
|
label: section.label,
|
|
75
76
|
subgroups: section.groups.map((g) => ({
|
|
76
77
|
label: g.label,
|
|
77
|
-
items: g.pages.map((p) => ({
|
|
78
|
+
items: g.pages.map((p) => ({
|
|
79
|
+
id: p.slug,
|
|
80
|
+
label: p.title,
|
|
81
|
+
badge: p.badge === undefined ? undefined : <span className="rounded border border-border/60 px-1 font-mono text-[10px] leading-tight text-muted-foreground/60">{p.badge}</span>,
|
|
82
|
+
})),
|
|
78
83
|
})),
|
|
79
84
|
}))
|
|
80
85
|
|
|
81
86
|
return (
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
<Split className="h-full">
|
|
88
|
+
<Pane inset="none" className="w-56">
|
|
89
|
+
<Sidebar className="w-full">
|
|
90
|
+
<PaneContent>
|
|
91
|
+
<SidebarNav
|
|
92
|
+
groups={navGroups}
|
|
93
|
+
activeId={page?.slug}
|
|
94
|
+
navLabel="Navegação da documentação"
|
|
95
|
+
onSelect={(slug) => go(`${basePath}/${slug}`)}
|
|
96
|
+
/>
|
|
97
|
+
</PaneContent>
|
|
98
|
+
</Sidebar>
|
|
99
|
+
</Pane>
|
|
100
|
+
<Pane key={page?.slug} grow inset="none" className="overflow-y-auto">
|
|
101
|
+
<div className="mx-auto max-w-3xl px-8 py-8">{page?.render()}</div>
|
|
102
|
+
</Pane>
|
|
103
|
+
</Split>
|
|
89
104
|
)
|
|
90
105
|
}
|
|
@@ -4,9 +4,9 @@ title: Actions & contratos
|
|
|
4
4
|
|
|
5
5
|
# Actions & contratos
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Uma action descreve uma operação que cliente e servidor precisam compreender da mesma forma.
|
|
8
|
+
Seu contrato reúne entidade, entrada, saída e metadados no package compartilhado. A API conecta
|
|
9
|
+
o handler e a web usa a mesma definição para executar ou renderizar a operação.
|
|
10
10
|
|
|
11
11
|
## Contrato primeiro
|
|
12
12
|
|
|
@@ -51,8 +51,9 @@ export const workspaceCreate = bindAction(workspaceCreateContract, {
|
|
|
51
51
|
|
|
52
52
|
## Ordem canônica dos campos
|
|
53
53
|
|
|
54
|
-
>
|
|
55
|
-
> `create-opus-action`
|
|
54
|
+
> Esta ordem mantém contratos previsíveis para leitura e geração. O `opus check` informa os
|
|
55
|
+
> campos que precisam ser reposicionados, e a skill `create-opus-action` já cria o contrato e o
|
|
56
|
+
> binding nessa estrutura.
|
|
56
57
|
|
|
57
58
|
```text
|
|
58
59
|
name → kind → (label / summary / messages / tags…)
|
|
@@ -130,7 +131,7 @@ descartável — vira o contrato que o backend honra.
|
|
|
130
131
|
import { useLookupAction } from '@softize/opus/client'
|
|
131
132
|
import { ActionForm } from '@softize/opus/ui/react'
|
|
132
133
|
|
|
133
|
-
//
|
|
134
|
+
// O contrato mantém a lista paginada tipada sem exigir uma definição paralela.
|
|
134
135
|
const { rows, fetchNextPage } = useLookupAction(workspaceListContract)
|
|
135
136
|
|
|
136
137
|
// Form contract-driven: os campos (fields) moram NO contrato.
|